Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 12 การวัดประสิทธิภาพเบื้องต้นและคุณภาพโค้ด
แผนบริหารการสอนประจำบทที่ 12
บทนี้ว่าด้วยการวัดประสิทธิภาพเบื้องต้นและคุณภาพโค้ดของภาษา C ซึ่งเป็นการประเมินว่าโปรแกรมทำงานมีประสิทธิภาพเพียงใดและเขียนได้ดีเพียงใด ต่อยอดจากการเรียงลำดับและการค้นหาในบทที่ 10 ฟังก์ชันในบทที่ 6 และตัวนับในบทที่ 11 โดยอธิบายการวัดประสิทธิภาพสองแนวทางคือการนับจำนวนรอบการทำงานซึ่งเปรียบเทียบซ้ำได้เมื่อโค้ด ข้อมูล นิยามหน่วย และสถานะเริ่มเหมือนกัน และการจับเวลาตาม runtime ด้วย clock() ซึ่งค่าผันแปรตามเครื่องและปริมาณงาน พร้อมหลักคุณภาพโค้ดที่เน้นการเลี่ยงการคำนวณซ้ำซ้อนและการสมดุลระหว่างความอ่านง่ายกับประสิทธิภาพ เพื่อให้พัฒนาโปรแกรมที่ถูกต้องตามหลักไวยากรณ์และมีประสิทธิภาพ
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO12.1 | วัดประสิทธิภาพของขั้นตอนวิธีด้วยการนับจำนวนรอบการทำงาน | งานข้อ 1 (ปฏิบัติการที่ 12 Lab A) | CLO3 |
| LLO12.2 | วัดเวลาการทำงานของโปรแกรมด้วย clock() และอธิบายว่าค่าขึ้นกับเครื่องและปริมาณงาน | งานข้อ 2 (ปฏิบัติการที่ 12 Lab B) | CLO3 |
| LLO12.3 | ปรับปรุงโค้ดให้มีประสิทธิภาพและคุณภาพดีขึ้น โดยสมดุลความอ่านง่ายกับประสิทธิภาพ | งานข้อ 3 (ปฏิบัติการที่ 12 Lab A/B) | CLO3 |
| LLO12.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO3 สามารถเขียนโค้ดได้ถูกต้องตามหลักไวยากรณ์ (Syntax) และมีประสิทธิภาพ (Efficiency)
บทที่ 12 เป็นจุดที่นำการค้นหาจากบทที่ 10 และตัวนับจากบทที่ 11 มาใช้วัดและเปรียบเทียบประสิทธิภาพของขั้นตอนวิธีอย่างเป็นรูปธรรม จึงเป็นสะพานเชื่อมไปสู่มาตรฐานการเขียนโค้ดในบทที่ 13 และโครงงานบูรณาการในบทที่ 15 ที่ต้องส่งมอบโปรแกรมที่มีคุณภาพและมีประสิทธิภาพ งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO3 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท
วิธีสอน
- ทบทวนการค้นหาแบบ linear และ binary จากบทที่ 10 และแนวคิดการนับรอบเปรียบเทียบ แล้วตั้งคำถามว่าจะวัดได้อย่างไรว่าวิธีใดทำงานน้อยรอบกว่า เพื่อนำเข้าสู่การวัดประสิทธิภาพด้วยตัวนับ
- สาธิตการใส่ตัวนับทั่วไป (global counter) ในฟังก์ชันค้นหาเพื่อนับจำนวนรอบการเปรียบเทียบ และชี้ให้เห็นว่าผลการนับเท่ากันทุกเครื่องเพราะไม่ขึ้นกับความเร็วของฮาร์ดแวร์
- สาธิตการจับเวลาด้วย
clock()จากtime.hและการแปลงเป็นวินาทีด้วยCLOCKS_PER_SECพร้อมย้ำว่าค่าเวลาที่ได้ผันแปรตามเครื่องและสภาพการทำงาน จึงใช้เปรียบเทียบคร่าว ๆ ไม่ใช่ค่าตายตัว - อธิบายหลักคุณภาพโค้ด ได้แก่ การเลี่ยงการคำนวณซ้ำซ้อนด้วยการเก็บผลไว้ในตัวแปร และการสมดุลระหว่างความอ่านง่ายกับประสิทธิภาพ โดยไม่ปรับโค้ดให้ซับซ้อนเกินจำเป็น
- ให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 12 และสไลด์สรุปการวัดประสิทธิภาพด้วยการนับรอบ การจับเวลา และหลักคุณภาพโค้ด
- ไฟล์โปรแกรมตัวอย่าง
ch12_count.cและch12_time.c
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 13 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 13 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | ใส่ตัวนับในฟังก์ชันค้นหาแล้วเทียบจำนวนรอบของ linear กับ binary บนข้อมูลเรียงแล้ว | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO12.1 | CLO3 |
| 2 | ใช้ clock() จับเวลาการทำงานของโปรแกรม และอธิบายว่าเหตุใดค่าจึงอาจเปลี่ยนระหว่างการรัน | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO12.2 | CLO3 |
| 3 | ปรับโค้ดที่คำนวณซ้ำซ้อนให้ดีขึ้นโดยยังอ่านง่าย พร้อมเหตุผล | ไฟล์ซอร์สโค้ด + คำอธิบาย | LLO12.3 | CLO3 |
เงื่อนไขผ่าน: งานข้อ 3 ต่อยอดจากไฟล์ของงานข้อ 1 หรือข้อ 2 ดังนั้นงานข้อ 1 หรือข้อ 2 ต้องคอมไพล์และรันได้ผลถูกต้องก่อนจึงจะประเมินการปรับปรุงในข้อ 3 ได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO12.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO12.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO12.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO12.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 1 | นับรอบถูกต้องและตีความได้ว่าวิธีใดทำงานน้อยรอบกว่า | นับรอบผิด หรือสรุปการเปรียบเทียบไม่ถูก |
งานข้อ 2 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ (checklist): รวม #include <time.h> ครบ, ใช้ clock() และหารด้วย CLOCKS_PER_SEC ถูกต้อง, อธิบายได้ว่าค่าเวลาผันแปรตามเครื่องและปริมาณงาน, จัดย่อหน้า 4 ช่องว่างและมีคอมเมนต์ส่วนหัว
งานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ: ลดการคำนวณซ้ำซ้อนได้จริง, คงความอ่านง่ายของโค้ดไว้, อธิบายเหตุผลของการปรับปรุงได้, เป็นไปตามมาตรฐานโค้ด
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
การเขียนโปรแกรมให้ได้ผลลัพธ์ถูกต้องเป็นเป้าหมายแรก แต่เมื่อข้อมูลมีขนาดใหญ่ขึ้น ประสิทธิภาพของโปรแกรมก็สำคัญไม่น้อยไปกว่าความถูกต้อง การวัดประสิทธิภาพคือการประเมินว่าโปรแกรมทำงานมากน้อยเพียงใดหรือใช้เวลาเท่าใด เพื่อใช้เปรียบเทียบและเลือกวิธีที่เหมาะสม บทที่ 10 ได้แนะนำการค้นหาแบบ linear และ binary พร้อมแนวคิดการนับรอบเปรียบเทียบ และบทที่ 11 ได้แนะนำการใช้ตัวนับและการทดสอบไปแล้ว บทนี้นำทั้งสองเรื่องมาใช้วัดประสิทธิภาพอย่างเป็นรูปธรรมด้วยสองแนวทาง คือการนับจำนวนรอบการทำงานซึ่งเปรียบเทียบซ้ำได้เมื่อโค้ด ข้อมูล นิยามหน่วย และสถานะเริ่มเหมือนกัน และการจับเวลาตาม runtime ด้วย clock() ซึ่งค่าผันแปร พร้อมหลักคุณภาพโค้ดที่ช่วยให้โปรแกรมทั้งเร็วและอ่านง่าย
ทักษะเรื่องการวัดประสิทธิภาพและคุณภาพโค้ดในบทนี้เป็นพื้นฐานของบทที่ตามมา การเขียนโค้ดที่มีประสิทธิภาพและมีคุณภาพเชื่อมโยงกับมาตรฐานการเขียนโค้ดในบทที่ 13 และโครงงานบูรณาการในบทที่ 15 ที่ต้องส่งมอบโปรแกรมซึ่งทั้งถูกต้อง มีประสิทธิภาพ และอ่านง่าย
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| การวัดประสิทธิภาพด้วยการนับจำนวนรอบการทำงาน | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
การจับเวลาด้วย clock() และการตีความค่าที่ผันแปร | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
| หลักคุณภาพโค้ด เลี่ยงการคำนวณซ้ำและสมดุลความอ่านง่ายกับประสิทธิภาพ | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องการค้นหาแบบ linear และ binary จากบทที่ 10 ฟังก์ชันจากบทที่ 6 และการใช้ตัวนับจากบทที่ 11 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ ผลการนับจำนวนรอบเป็นค่าคงที่เหมือนกันทุกเครื่อง แต่ค่าเวลาที่ได้จาก clock() ผันแปรตามเครื่องและสภาพการทำงาน จึงไม่ใช่ค่าตายตัว หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ค่าเวลาที่แสดงอาจต่างออกไป แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
ทบทวน บทที่ 11 การดีบักและการทดสอบระดับหน่วย โดยเก็บชุดทดสอบที่ผ่านไว้ก่อนปรับโค้ด การวัดวิธีที่ตอบผิดไม่ได้ตอบว่าวิธีใดทำงานเดียวกันได้เร็วกว่า
ใบงานเป็นตารางสำหรับคัดลงสมุดหรือเอกสารของตน ไม่ใช่ช่องส่งคำตอบบนเว็บไซต์ ใช้ลำดับ กำหนดงาน → ตรวจผล → ระบุหน่วยและช่วงวัด → รันซ้ำ → รายงานข้อจำกัด ไม่คัดลอกเวลาตัวอย่างเป็นผลของตน
ข้อมูลเป็นข้อมูลสมมติ กิจกรรมเพิ่มไม่เปลี่ยน LLO12.1–12.3 งานส่ง หรือสัดส่วนคะแนน แผนเวลาและแนวคำตอบอยู่เฉพาะส่วนผู้สอน เวลาแผนสอนไม่ใช่เวลา CPU ของโปรแกรมและไม่ใช่ผลวัดการสอนจริง
12.1 การวัดประสิทธิภาพด้วยการนับรอบ
ระดับที่ 1: การวัดประสิทธิภาพด้วยการนับรอบคือการนับจำนวนครั้งที่โปรแกรมทำงานสำคัญ เช่น จำนวนครั้งที่เปรียบเทียบค่า เพื่อประเมินว่าขั้นตอนวิธีหนึ่งทำงานมากหรือน้อยเพียงใด
ระดับที่ 2: วิธีที่ใช้ได้ทันทีคือประกาศตัวแปรนับทั่วไป (global counter) แล้วเพิ่มค่าตัวนับหนึ่งครั้งทุกครั้งที่เกิดการทำงานที่ต้องการวัด เช่น วางคำสั่ง count++; ไว้ต้นลูปหรือต้นฟังก์ชันค้นหา เมื่อโปรแกรมทำงานเสร็จ ค่าของตัวนับคือจำนวนรอบทั้งหมด ข้อดีของการนับรอบคือผลซ้ำได้สำหรับโค้ด ข้อมูล หน่วย และสถานะเริ่มเดียวกัน เพราะนับจากตรรกะของขั้นตอนวิธี ไม่ได้ขึ้นกับความเร็วของฮาร์ดแวร์หรือสภาพการทำงานของเครื่อง การนับรอบต่อยอดจากการใช้ตัวนับในบทที่ 5 และบทที่ 11 โดยตรง
int compare_count = 0;
/* ... */
compare_count++; /* นับหนึ่งครั้งทุกการเปรียบเทียบ */ระดับที่ 3: การนับรอบใช้เปรียบเทียบขั้นตอนวิธีได้อย่างเป็นรูปธรรม เช่น การค้นหาเลข 30 ในข้อมูลเรียงแล้ว 16 ตัวจากบทที่ 10 เมื่อใส่ตัวนับในทั้ง linear search และ binary search จะพบว่า linear search ใช้ 16 รอบในกรณีแย่สุด ขณะที่ binary search ใช้เพียง 5 รอบ เพราะตัดข้อมูลครึ่งหนึ่งทิ้งทุกรอบ ตัวเลขนี้ยืนยันเชิงปริมาณว่า binary search ทำงานน้อยรอบกว่าบนข้อมูลเรียงแล้ว กับดักที่พบบ่อยคือการลืมรีเซ็ตตัวนับเป็นศูนย์ก่อนวัดใหม่ และการวางคำสั่งเพิ่มตัวนับผิดจุดจนนับเกินหรือขาด
คำถามตรวจความเข้าใจ 12.1: เหตุใดการนับจำนวนรอบจึงให้ผลเท่ากันทุกเครื่อง ต่างจากการจับเวลาจริง
ใบงาน 1: ใช้หน่วยเดียวกันก่อนเทียบ
| ตัวเลขที่รายงาน | นับอะไรหนึ่งครั้ง | รวมงานส่วนใด | ยังไม่บอกอะไร |
|---|---|---|---|
| probe ของการค้นหา | |||
| ตัวดำเนินการเปรียบเทียบค่าข้อมูล | |||
| จำนวนสลับสมาชิก | |||
| วินาทีจาก clock |
ตัวนับ Lab A เพิ่มหนึ่งครั้งต่อสมาชิกที่นำมาตรวจ จึงเป็น probe ไม่ใช่จำนวน == และ < ทั้งหมด ส่วน binary หนึ่ง probe อาจทำการเทียบค่ามากกว่าหนึ่งครั้ง หากจะนับแบบใหม่ต้องเปลี่ยนตำแหน่งเพิ่มตัวนับและป้ายหน่วยให้ตรง
จำนวนงานซ้ำได้เมื่อใช้โค้ด ข้อมูล จำนวนคำค้น และสถานะเริ่มเดียวกัน รวมถึงไม่มีพฤติกรรมไม่กำหนด ไม่ยกข้อความว่า “เท่ากันทุกเครื่อง” ไปครอบคลุมการเปลี่ยนข้อมูลหรือการลืมรีเซ็ตตัวนับ
ตัวอย่างพร้อมใช้: ตัวนับเฉพาะการเรียกไม่ค้างจากรอบก่อน
สร้างไฟล์ local_counters.c คืนทั้งดัชนีและจำนวน probe ในระเบียนผลลัพธ์ แต่ละการเรียกเริ่มตัวนับใหม่ ต่างจาก global counter ที่ผู้เรียกอาจลืมรีเซ็ต
/* local_counters.c : counters belong to each search result */
#include <stdio.h>
struct SearchResult {
int index;
int probes;
};
struct SearchResult linear_probe(const int values[], int count, int target);
struct SearchResult binary_probe(const int values[], int count, int target);
int main(void)
{
int values[16];
const int targets[4] = {0, 30, 31, 30};
for (int i = 0; i < 16; ++i) {
values[i] = i * 2;
}
for (int i = 0; i < 4; ++i) {
struct SearchResult a = linear_probe(values, 16, targets[i]);
struct SearchResult b = binary_probe(values, 16, targets[i]);
printf("target=%d linear=%d/%d binary=%d/%d\n",
targets[i], a.index, a.probes, b.index, b.probes);
}
return 0;
}
struct SearchResult linear_probe(const int values[], int count, int target)
{
struct SearchResult result = {-1, 0};
for (int i = 0; i < count; ++i) {
++result.probes;
if (values[i] == target) {
result.index = i;
return result;
}
}
return result;
}
struct SearchResult binary_probe(const int values[], int count, int target)
{
struct SearchResult result = {-1, 0};
int low = 0;
int high = count - 1;
while (low <= high) {
int middle = low + (high - low) / 2;
++result.probes;
if (values[middle] == target) {
result.index = middle;
return result;
}
if (values[middle] < target) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return result;
}ผลแต่ละคู่พิมพ์เป็น index/probes ลองทำนายสองคำค้น 30 ที่ซ้ำกัน ถ้าไม่มีการแก้ข้อมูลหรือเปลี่ยนโค้ด ทั้งสองคำค้นต้องให้ผลและจำนวน probe เดิม ไม่สะสมจากคำถามก่อนหน้า
12.2 การวัดเวลาด้วย clock()
ระดับที่ 1: การวัดเวลาคือการจับเวลาตั้งแต่เริ่มทำงานจนจบ แล้วคำนวณว่าช่วงงานนั้นใช้เวลาไปเท่าใด เพื่อประเมินประสิทธิภาพในหน่วยเวลาจริง
ระดับที่ 2: ภาษา C มีฟังก์ชัน clock() ในส่วนหัว time.h สำหรับจับเวลาของหน่วยประมวลผล วิธีใช้คือบันทึกค่าเวลาเริ่มด้วย clock() ก่อนช่วงงาน บันทึกค่าเวลาจบหลังช่วงงาน แล้วหาผลต่าง จากนั้นหารด้วยค่าคงที่ CLOCKS_PER_SEC เพื่อแปลงเป็นหน่วยวินาที ค่าคงที่ CLOCKS_PER_SEC บอกจำนวนหน่วยนับเวลาต่อหนึ่งวินาที จึงใช้แปลงผลต่างของตัวนับเวลาให้เป็นวินาทีได้ถูกต้อง
clock_t start = clock();
if (start == (clock_t)-1) {
puts("Clock unavailable");
return 1;
}
/* ช่วงงานที่ต้องการจับเวลา */
clock_t end = clock();
if (end == (clock_t)-1 || end < start) {
puts("Clock interval unavailable");
return 1;
}
double seconds = ((double)end - (double)start) / CLOCKS_PER_SEC;ระดับที่ 3: ค่าเวลาที่ได้จาก clock() ผันแปรตามเครื่องและสภาพการทำงาน เช่น ความเร็วของหน่วยประมวลผล ภาระงานอื่นที่ทำงานพร้อมกัน และการปรับแต่งของคอมไพเลอร์ ดังนั้นเมื่อรันโปรแกรมเดิมหลายครั้ง ค่าเวลาที่ได้อาจต่างกัน หรือแสดงเท่ากันเมื่อความละเอียดแยกไม่ชัด ค่าเวลาจึงใช้เปรียบเทียบคร่าว ๆ ว่างานใดใช้เวลามากกว่ากัน ไม่ใช่ค่าตายตัวที่ยึดเป็นตัวเลขเดียว ต่างจากการนับรอบที่ให้ค่าคงที่ กับดักที่พบบ่อยคือการลืมรวม #include <time.h> การหารด้วย CLOCKS_PER_SEC ผิด และการยึดค่าเวลาที่วัดได้ครั้งเดียวเป็นค่าตายตัว
คำถามตรวจความเข้าใจ 12.2: CLOCKS_PER_SEC ใช้ทำอะไรในการคำนวณเวลา และเหตุใดค่าเวลาที่วัดได้จึงอาจเปลี่ยนระหว่างการรัน
ใบงาน 2: clock วัดอะไรใน runtime ที่ใช้
ตาม ISO C ฟังก์ชัน clock ประมาณ processor time แต่ Microsoft CRT ระบุว่ารายงาน elapsed wall-clock time ตั้งแต่เริ่ม CRT จึงต้องตรวจ runtime ที่ใช้ ไม่ถือว่าค่า clock บนทุกระบบเป็นเวลา CPU แบบเดียวกัน
| สิ่งที่ต้องบันทึก | ค่าที่ใช้จริง |
|---|---|
| ระบบปฏิบัติการและ runtime | |
| คอมไพเลอร์/รุ่นและตัวเลือก | |
| CLOCKS_PER_SEC | |
| ช่วงงานที่จับเวลา | |
| รวมการอ่าน input หรือพิมพ์ผลหรือไม่ | |
| ทำอย่างไรถ้า clock คืน -1 หรือช่วงเวลาใช้ไม่ได้ |
CLOCKS_PER_SEC เป็นตัวแปลงหน่วย ไม่ใช่หลักฐานความละเอียดจริง เช่น พิมพ์ทศนิยมหกตำแหน่งไม่ได้ทำให้ตัวจับเวลาแยกได้ถึงหนึ่งไมโครวินาทีเสมอ การเห็นศูนย์จึงยังไม่ยืนยันว่าไม่ใช้เวลา
12.3 คุณภาพโค้ดและการปรับปรุง
ระดับที่ 1: โค้ดที่มีคุณภาพคือโค้ดที่อ่านง่ายและไม่ทำงานซ้ำซ้อนโดยไม่จำเป็น ซึ่งช่วยให้ทั้งแก้ไขง่ายและทำงานมีประสิทธิภาพ
ระดับที่ 2: เทคนิคพื้นฐานหนึ่งคือการเลี่ยงการคำนวณซ้ำซ้อน หากนิพจน์ใดให้ผลเท่าเดิมทุกรอบของลูป ควรคำนวณครั้งเดียวแล้วเก็บผลไว้ในตัวแปรก่อนลูป แทนการคำนวณใหม่ซ้ำ ๆ ภายในลูป ตัวอย่างเช่น หากขอบเขตของลูปต้องเรียกฟังก์ชันที่ให้ค่าคงที่ ควรเก็บค่านั้นไว้ในตัวแปรก่อน แล้วใช้ตัวแปรเป็นเงื่อนไขของลูป วิธีนี้ลดจำนวนการทำงานลงโดยไม่เปลี่ยนผลลัพธ์
int limit = compute_limit(); /* คำนวณครั้งเดียวก่อนลูป */
for (int i = 0; i < limit; i++) {
/* ... */
}ระดับที่ 3: การปรับปรุงโค้ดต้องสมดุลระหว่างความอ่านง่ายกับประสิทธิภาพ การปรับให้เร็วขึ้นไม่ควรทำให้โค้ดซับซ้อนจนอ่านและแก้ไขยาก ควรปรับเฉพาะจุดที่มีผลจริงต่อประสิทธิภาพ และวัดด้วยการนับรอบหรือจับเวลาเพื่อยืนยันว่าการปรับนั้นช่วยได้จริง ไม่ควรปรับโค้ดให้ซับซ้อนเกินจำเป็นเพียงเพราะคาดเดาว่าจะเร็วขึ้น การคงความอ่านง่ายไว้เป็นส่วนหนึ่งของคุณภาพโค้ดที่เชื่อมโยงกับมาตรฐานการเขียนโค้ดในบทที่ 13
คำถามตรวจความเข้าใจ 12.3: การปรับโค้ดให้เร็วขึ้นควรคำนึงถึงความอ่านง่ายอย่างไร
12.4 การเลือกวิธีให้เหมาะกับโจทย์
ระดับที่ 1: ไม่มีวิธีใดวิธีเดียวที่ดีที่สุดเสมอ การเลือกขั้นตอนวิธีควรพิจารณาจากลักษณะของโจทย์และข้อมูล
ระดับที่ 2: ปัจจัยที่ใช้เลือกวิธี ได้แก่ ขนาดของข้อมูลและสภาพของข้อมูลว่าเรียงแล้วหรือไม่ เช่น binary search ทำงานน้อยรอบกว่า linear search มากเมื่อข้อมูลมีขนาดใหญ่ แต่ใช้ได้ก็ต่อเมื่อข้อมูลเรียงแล้วเท่านั้น หากข้อมูลยังไม่เรียงและมีจำนวนน้อย การค้นหาแบบ linear ที่เรียบง่ายอาจเหมาะสมกว่า เพราะไม่ต้องเสียงานเรียงข้อมูลก่อน
ระดับที่ 3: การตัดสินใจเลือกวิธีควรอยู่บนข้อมูลจริง ไม่ใช่การคาดเดา เครื่องมือวัดประสิทธิภาพทั้งสองแบบในบทนี้ช่วยได้ คือการนับรอบบอกจำนวนการทำงานที่ไม่ขึ้นกับเครื่อง และการจับเวลาบอกเวลาจริงบนเครื่องที่ใช้งาน การนำผลการวัดมาประกอบกับความอ่านง่ายและความเหมาะสมกับขนาดข้อมูล ช่วยให้เลือกวิธีได้อย่างมีเหตุผลและสมดุล
คำถามตรวจความเข้าใจ 12.4: ปัจจัยใดบ้างที่ควรพิจารณาในการเลือกระหว่าง linear search กับ binary search
ตัวอย่างรวมต้นทุน: แยกการเตรียมกับการค้นให้เห็น
สร้างไฟล์ preparation_cost.c ใช้ข้อมูล 10 ถึง 1 และรับเป้าหมายหนึ่งค่า โปรแกรมนับการเปรียบเทียบค่าข้อมูลด้วย == หรือ < อย่างละหนึ่งครั้ง ไม่รวมเงื่อนไขลูปหรือการสลับ เพื่อให้หน่วยที่นำมาบวกเหมือนกัน
/* preparation_cost.c : include preparation using the same counting unit */
#include <stdio.h>
struct Result {
int index;
int key_tests;
};
struct Result linear_test(const int values[], int count, int target);
struct Result binary_test(const int values[], int count, int target);
int sort_test(int values[], int count);
int main(void)
{
const int original[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int sorted[10];
int target;
if (scanf("%d", &target) != 1) {
puts("Input error");
return 1;
}
for (int i = 0; i < 10; ++i) {
sorted[i] = original[i];
}
struct Result linear = linear_test(original, 10, target);
int prepare = sort_test(sorted, 10);
struct Result binary = binary_test(sorted, 10, target);
printf("linear index=%d key_tests=%d\n", linear.index, linear.key_tests);
printf("sort key_tests=%d\n", prepare);
printf("binary index=%d key_tests=%d\n", binary.index, binary.key_tests);
printf("prepared total_key_tests=%d\n", prepare + binary.key_tests);
return 0;
}
struct Result linear_test(const int values[], int count, int target)
{
struct Result result = {-1, 0};
for (int i = 0; i < count; ++i) {
++result.key_tests;
if (values[i] == target) {
result.index = i;
return result;
}
}
return result;
}
struct Result binary_test(const int values[], int count, int target)
{
struct Result result = {-1, 0};
int low = 0;
int high = count - 1;
while (low <= high) {
int middle = low + (high - low) / 2;
++result.key_tests;
if (values[middle] == target) {
result.index = middle;
return result;
}
++result.key_tests;
if (values[middle] < target) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return result;
}
int sort_test(int values[], int count)
{
int tests = 0;
for (int i = 0; i < count - 1; ++i) {
int minimum = i;
for (int j = i + 1; j < count; ++j) {
++tests;
if (values[j] < values[minimum]) {
minimum = j;
}
}
int temporary = values[i];
values[i] = values[minimum];
values[minimum] = temporary;
}
return tests;
}ทดลองเป้าหมาย 1, 10 และ 99 พร้อมระบุว่าดัชนี linear อ้าง original ส่วน binary อ้าง sorted จำนวน key_tests ที่บวกกันเป็นจำนวนงานที่นิยามไว้ ไม่ได้อ้างว่าการเปรียบเทียบทุกครั้งใช้เวลา CPU เท่ากัน
ตัวอย่างนี้ค้นหนึ่งครั้งและใช้ selection sort ตามบทเรียน ไม่ใช่ข้อเสนอให้ใช้ selection sort กับข้อมูลขนาดใหญ่เสมอ เมื่อจำนวนคำค้นหรือรูปแบบข้อมูลเปลี่ยนต้องตั้งการทดลองใหม่
12.5 กรณีศึกษาต่อเนื่อง: ค้นหาครั้งเดียวกับค้นหาซ้ำหลายครั้ง
คำว่า “เร็วกว่า” ต้องมีขอบเขตการเปรียบเทียบ กำหนดว่าค้นหาข้อมูลแบบใด จำนวนรายการเท่าไร นับงานส่วนไหน และข้อมูลเรียงอยู่ก่อนหรือยัง หากวิธีหนึ่งได้อาร์เรย์เรียงฟรี แต่วิธีอื่นถูกนับรวมการเตรียมข้อมูล การเปรียบเทียบจะตอบคนละคำถาม
12.5.1 นิยามหน่วยงานก่อนนับ
สำหรับ selection sort แบบพื้นฐาน จำนวนการเปรียบเทียบเมื่อมี 5 สมาชิกคือ 4 + 3 + 2 + 1 เท่ากับ 10 เมื่อมี 10 สมาชิกเป็น 45 และเมื่อมี 20 สมาชิกเป็น 190 ตัวเลขนี้เป็นจำนวนเปรียบเทียบของอัลกอริทึม ไม่ใช่มิลลิวินาที การเพิ่มขนาดเป็นสองเท่าไม่ได้ทำให้จำนวนงานเพิ่มสองเท่าเสมอ
| ขนาดข้อมูล | selection sort: เปรียบเทียบ | linear search: มากที่สุดเมื่อค้นหนึ่งครั้ง |
|---|---|---|
| 5 | 10 | 5 |
| 10 | 45 | 10 |
| 20 | 190 | 20 |
ถ้าต้องเรียง 20 รายการด้วยวิธีนี้เพื่อใช้ binary search เพียงครั้งเดียว ต้นทุนเตรียม 190 การเปรียบเทียบยังต้องอยู่ในภาพรวม ส่วนถ้าข้อมูลเรียงอยู่แล้วและค้นหลายครั้ง คำถามเปลี่ยนไป ต้องแยกต้นทุนเริ่มต้นออกจากต้นทุนต่อคำค้น ไม่เอาหน่วยเปรียบเทียบไปเท่ากับต้นทุนจริงของทุกคำสั่งโดยอัตโนมัติ
NIST: Selection Sort และ NIST: Binary Search ให้พื้นฐานเรื่องการเติบโตของงาน ส่วนการตัดสินสำหรับโปรแกรมจริงต้องพิจารณาขนาดข้อมูลและสิ่งที่วัดเพิ่มเติม
12.6 วัด clock อย่างมีข้อจำกัดและไม่แต่งผลทดลอง
ตาม ISO C clock() ประมาณ processor time ที่โปรแกรมใช้ แต่ Microsoft CRT ใช้ elapsed wall-clock time จึงต้องอ้างอิง runtime ไม่ใช้ข้อความว่าเป็น CPU time เหมือนกันทุกระบบ การอ่านข้อมูลจากแป้นพิมพ์ การพัก และภาระระบบอาจทำให้เวลาแต่ละชนิดต่างกัน ต้องนำผลต่างของค่า clock_t มาหารด้วย CLOCKS_PER_SEC เพื่อแปลงเป็นวินาที ไม่ถือว่าหนึ่ง tick เท่ากับหนึ่งมิลลิวินาทีในทุกระบบ
#include <stdio.h>
#include <time.h>
int main(void)
{
volatile unsigned long checksum = 0;
clock_t start = clock();
if (start == (clock_t)-1) {
puts("Clock unavailable");
return 1;
}
for (unsigned long i = 0; i < 1000000UL; ++i) {
checksum += i % 7UL;
}
clock_t finish = clock();
if (finish == (clock_t)-1 || finish < start) {
puts("Clock interval unavailable");
return 1;
}
printf("checksum=%lu\n", checksum);
printf("clock seconds=%.6f\n",
((double)finish - (double)start) / CLOCKS_PER_SEC);
return 0;
}ผล checksum เป็น 2999997 ส่วนเวลาขึ้นกับเครื่อง เครื่องมือ และการรันจริง จึงไม่มีตัวเลขเวลาตายตัวให้คัดลอก ตัวอย่างใช้ช่วงทดสอบสั้นและค่าผลรวมอยู่ในช่วงขั้นต่ำของ unsigned long การใช้ volatile ทำให้งานมีการอ่านเขียนที่สังเกตได้ แต่เพิ่มต้นทุนของตนเอง จึงไม่ใช่สูตร benchmark ที่ทำให้ทุกอัลกอริทึมเปรียบเทียบอย่างยุติธรรมทันที
หากได้เวลา 0.000000 ไม่ได้พิสูจน์ว่าโปรแกรมทำงานโดยไม่ใช้เวลา อาจเป็นเพราะช่วงงานสั้นกว่าความละเอียดของตัวจับเวลา ควรใช้การนับงานประกอบ เพิ่มงานอย่างควบคุม และไม่เพิ่มข้อมูลจนเกินความจุอาร์เรย์หรือช่วงตัวเลขเพื่อให้จับเวลาได้
ข้อเท็จจริงเรื่อง CPU time และหน่วยอ้างอิงจาก glibc: CPU Time Inquiry ค่า macro สำหรับแปลงหน่วยไม่เท่ากับความละเอียดจริงของนาฬิกา และควรตรวจกรณีที่ระบบไม่ให้เวลาตามที่โปรแกรมขอได้
12.6.1 ออกแบบการทดลองที่ทำซ้ำและเปรียบเทียบได้
ใช้ข้อมูลชุดเดียวกันหรือสำเนาที่มีเนื้อหาเหมือนกันทั้งสองวิธี อย่าปล่อยให้วิธีแรกเรียงข้อมูลแล้วส่งอาร์เรย์ที่เรียงแล้วให้วิธีที่สองโดยไม่ได้ตั้งใจ ถ้าวัดเฉพาะการค้น ให้เตรียมข้อมูลก่อนเริ่มจับเวลาและบอกชัดว่าไม่นับการเรียง หากวัดทั้งกระบวนการให้รวมการเตรียมเหมือนกัน
แยกการพิมพ์ผลออกจากช่วงที่ต้องการวัด เพราะการแสดงข้อความจำนวนมากอาจกลบต้นทุนคำนวณ บันทึกผลหลายครั้งในสภาพเดียวกันพร้อมช่วงที่สังเกตได้ ไม่เลือกเฉพาะครั้งที่เร็วที่สุดของวิธีที่ตนเองชอบ และไม่อ้างความต่างเล็กกว่าความละเอียดที่เครื่องมือให้เป็นข้อสรุปหนักแน่น
ตัวอย่างพร้อมใช้: เก็บเวลาสามครั้งโดยไม่แต่งตัวเลข
สร้างไฟล์ clock_samples.c รับ units ช่วง 0–10000 แต่ละหน่วยทำงาน 100 รอบ สูงสุดหนึ่งล้านรอบต่อครั้ง วัดสามครั้งและตรวจ checksum เท่ากันก่อนนำเวลามาอภิปราย
/* clock_samples.c : repeatable workload, variable observed time */
#include <stdio.h>
#include <time.h>
int main(void)
{
int units;
if (scanf("%d", &units) != 1) {
puts("Input error");
return 1;
}
if (units < 0 || units > 10000) {
puts("Range error");
return 1;
}
unsigned long iterations = (unsigned long)units * 100UL;
printf("CLOCKS_PER_SEC=%.0f\n", (double)CLOCKS_PER_SEC);
for (int sample = 1; sample <= 3; ++sample) {
volatile unsigned long checksum = 0;
clock_t start = clock();
if (start == (clock_t)-1) {
puts("Clock unavailable");
return 1;
}
for (unsigned long i = 0; i < iterations; ++i) {
checksum += i % 7UL;
}
clock_t finish = clock();
if (finish == (clock_t)-1 || finish < start) {
puts("Clock interval unavailable");
return 1;
}
double seconds = ((double)finish - (double)start) / CLOCKS_PER_SEC;
printf("sample=%d checksum=%lu clock_seconds=%.6f\n",
sample, checksum, seconds);
}
return 0;
}ทดลอง units เป็น 0, 1, 100 และ 10000 บันทึกค่าที่ได้จริง อย่ากำหนดว่าทั้งสามครั้งต้องต่างกัน เพราะอาจได้ค่าเท่ากันหรือศูนย์ตามความละเอียดและสภาพแวดล้อม
ตัวอย่างใช้ volatile ให้มีการอ่านเขียนที่สังเกตได้ระหว่างลูป แต่ volatile เพิ่มงานของตนเอง ไม่ใช่เครื่องมือที่ทำให้การเปรียบเทียบอัลกอริทึมทุกชนิดยุติธรรมโดยอัตโนมัติ
การแปลงเวลาเป็น double ก่อนลบช่วยไม่ลบในชนิด clock_t ที่อาจล้น แต่ช่วงเวลาสั้นยังถูกจำกัดด้วยความละเอียดของนาฬิกาและการแทนค่าตัวเลข ตัวอย่างไม่กู้การวนกลับของนาฬิกา แต่ปฏิเสธช่วงที่ใช้ไม่ได้
12.7 คุณภาพโค้ดกับเวิร์กช็อปเลือกวิธีที่เหมาะสม
12.7.1 ลดงานซ้ำโดยรักษาความหมาย
โปรแกรมแสดงส่วนต่างจากค่าเฉลี่ยในบทที่ 7 สามารถคำนวณค่าเฉลี่ยครั้งเดียวก่อนลูป แทนการหาผลรวมทั้งอาร์เรย์ใหม่สำหรับทุกสมาชิก การแก้แบบนี้มีเหตุผลที่พิสูจน์ได้ว่าค่าภาพรวมไม่เปลี่ยน และลดจำนวนการอ่านข้อมูลจากงานซ้อนเป็นงานสองรอบ
แต่การเก็บผลไว้ใช้ซ้ำต้องรู้ว่าเมื่อใดข้อมูลเปลี่ยน หากเพิ่มคะแนนใหม่แล้วใช้ค่าเฉลี่ยเก่า ผลจะผิดแม้เร็วขึ้น ต้องกำหนดจุดคำนวณใหม่ให้สัมพันธ์กับการแก้ข้อมูล การปรับประสิทธิภาพที่ทำลายความถูกต้องไม่ใช่การปรับปรุงที่ยอมรับได้
12.7.2 เวิร์กช็อปสามสถานการณ์
สถานการณ์แรกมีข้อมูลไม่เกินสิบรายการและค้นเพียงครั้งเดียว ให้เสนอวิธีที่เรียบง่ายและอธิบายต้นทุนเตรียม สถานการณ์ที่สองมีข้อมูลเรียงแล้วและคำค้นหลายครั้ง ให้ trace การลดช่วงของ binary search สถานการณ์ที่สามมีการเพิ่มข้อมูลทุกครั้งก่อนค้น ให้พิจารณาต้นทุนรักษาลำดับ ไม่เลือกวิธีจากชื่อที่ดูทันสมัยกว่า
ทำตารางทดลองแยกจำนวนเปรียบเทียบ จำนวนสลับ เวลาที่สังเกต และข้อจำกัดของข้อมูล เก็บผลกรณีเรียงแล้ว เรียงย้อน และมีค่าซ้ำ สำหรับผลที่เปลี่ยนตามเครื่องให้ใช้ตัวเลขที่วัดเองเท่านั้น ไม่คัดลอกตัวอย่างเป็นผลทดสอบของตน
สุดท้ายเลือกโค้ดหนึ่งส่วนมาปรับชื่อ แยกฟังก์ชัน หรือกำจัดการคำนวณซ้ำ รันทดสอบเดิมก่อนและหลังแล้วรายงานว่าพฤติกรรมใดต้องเหมือนเดิม สิ่งที่ได้อาจเป็นความอ่านง่ายและตรวจง่ายโดยเวลาไม่ต่างอย่างวัดได้ ซึ่งยังเป็นการพัฒนาคุณภาพที่มีเหตุผล หากระบุหลักฐานตรงกับสิ่งที่เปลี่ยนจริง
ตัวอย่างปรับโค้ด: คำนวณภาพรวมครั้งเดียวเมื่อข้อมูลไม่เปลี่ยน
สร้างไฟล์ cached_mean.c รับ n ช่วง 1–10 และค่าข้อมูล n ตัวช่วง -100 ถึง 100 เปรียบเทียบส่วนต่างจากค่าเฉลี่ยแบบคำนวณซ้ำกับแบบเก็บค่าเฉลี่ย โดยตรวจว่าผลของแต่ละสมาชิกยังเท่ากัน
/* cached_mean.c : count the avoided work and preserve each result */
#include <stdio.h>
int mean_calls = 0;
int mean_reads = 0;
double mean_score(const int values[], int count);
int main(void)
{
int values[10];
double repeated[10];
double cached[10];
int count;
if (scanf("%d", &count) != 1) {
puts("Input error");
return 1;
}
if (count < 1 || count > 10) {
puts("Range error");
return 1;
}
for (int i = 0; i < count; ++i) {
if (scanf("%d", &values[i]) != 1) {
puts("Input error");
return 1;
}
if (values[i] < -100 || values[i] > 100) {
puts("Range error");
return 1;
}
}
mean_calls = 0;
mean_reads = 0;
for (int i = 0; i < count; ++i) {
repeated[i] = values[i] - mean_score(values, count);
}
printf("repeated calls=%d reads=%d\n", mean_calls, mean_reads);
mean_calls = 0;
mean_reads = 0;
double mean = mean_score(values, count);
for (int i = 0; i < count; ++i) {
cached[i] = values[i] - mean;
}
printf("cached calls=%d reads=%d\n", mean_calls, mean_reads);
for (int i = 0; i < count; ++i) {
if (repeated[i] != cached[i]) {
puts("Result mismatch");
return 1;
}
}
puts("same=1");
return 0;
}
/* Positive count, valid initialized elements, and sum fitting int. */
double mean_score(const int values[], int count)
{
++mean_calls;
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += values[i];
++mean_reads;
}
return (double)sum / count;
}ตัวนับนี้นับงานตามซอร์สโค้ด ไม่ใช่จำนวนการอ่าน RAM จริงหลังคอมไพเลอร์ปรับโค้ด
reads นับเฉพาะการอ่านสมาชิกในส่วนหาค่าเฉลี่ย ไม่รวมทุกการอ่านของโปรแกรม สำหรับ n เป็น 5 แบบแรกอ่านส่วนนี้ 25 ครั้ง ส่วนแบบเก็บค่าเฉลี่ยอ่าน 5 ครั้ง แต่ยังต้องทำลูปหาส่วนต่างและตรวจผล
สองฉบับคำนวณจากข้อมูลที่ไม่เปลี่ยนและใช้สูตรเดียวกัน จึงคาดว่าค่าส่วนต่างที่เก็บจะตรงกันได้ในตัวอย่างนี้ ไม่ใช้ == กับการคำนวณทศนิยมที่ต่างสูตรทุกงานโดยไม่มีเหตุผล
ถ้าเพิ่มข้อมูลหรือแก้ค่าแล้วใช้ mean เก่า การเก็บผลไว้จะเปลี่ยนความหมาย ให้เขียนจุดที่ต้องคำนวณใหม่ก่อนเพิ่มความสามารถแก้ข้อมูล ไม่ถือว่าการลดการเรียกฟังก์ชันเป็นผลดีเมื่อคำตอบผิด
ใบงาน 4: แก้เพื่อความอ่านง่ายและลดงานโดยมีหลักฐาน
| การเปลี่ยนที่เสนอ | พฤติกรรมที่ต้องคงเดิม | หลักฐานก่อน/หลัง | ผลดีที่กล่าวได้ |
|---|---|---|---|
| คืนตัวนับในระเบียนผลลัพธ์แทน global | |||
| เก็บค่าเฉลี่ยก่อนลูป | |||
| เปลี่ยนชื่อ min_idx ให้ตรงหน้าที่ | |||
| แยก input ออกจากช่วงวัด |
การย้าย compute_limit ออกจากเงื่อนไขลูปทำได้เมื่อผลไม่เปลี่ยนระหว่างลูปและไม่มีผลข้างเคียงที่จำเป็นต้องเกิดซ้ำ ถ้าฟังก์ชันอ่านข้อมูลใหม่หรือเปลี่ยนสถานะทุกครั้ง การย้ายอาจเปลี่ยนคำตอบ
การปรับชื่อหรือแยกหน้าที่อาจช่วยให้ตรวจง่ายโดยเวลาไม่ต่างอย่างวัดได้ ให้รายงานผลดีนั้นตรง ๆ และรันทดสอบสำคัญจากบทที่ 11 ก่อน/หลัง ไม่ใช้คำว่า “เร็วขึ้น” แทนคุณภาพทุกด้าน
12.8 ตัวอย่างเปรียบเทียบ: ผลทดลองที่ยังสรุปไม่ได้
สมมติรันวิธี A ได้ 0.001, 0.002 และ 0.001 วินาที ส่วนวิธี B ได้ 0.001, 0.001 และ 0.002 วินาที ข้อมูลตัวเลขนี้เป็นสถานการณ์สมมติเพื่อฝึกตีความ ไม่ใช่ผล benchmark ที่วัดจริง ช่วงที่ได้ทับซ้อนกันมาก จึงยังไม่เหมาะกล่าวว่า B เร็วกว่าอย่างแน่นอนเพราะเลือกเฉพาะรอบที่ตนพอใจ
สิ่งที่ควรรายงานคือเครื่องมือและขอบเขตงาน จำนวนรอบ ผลที่สังเกต และข้อจำกัดเรื่องความละเอียด จากนั้นใช้จำนวนการเปรียบเทียบประกอบเพื่ออธิบายแนวโน้ม เมื่อขนาดข้อมูลยังเล็ก ค่าใช้จ่ายเรียกฟังก์ชันและรายละเอียดเครื่องอาจมีผลมากกว่าความต่างที่ต้องการศึกษา
อีกสถานการณ์หนึ่ง วิธี A ให้ผลถูกและเวลา 0.002 ส่วนวิธี B เวลา 0.001 แต่ข้ามสมาชิกท้ายไปหนึ่งตัว วิธี B ไม่ใช่คู่แข่งที่ทำงานเดียวกันครบ การตรวจความถูกต้องต้องมาก่อนการวัด และควรตรวจผลหลังรันทุกรูปแบบการทดลอง ไม่เชื่อเพราะโค้ดหน้าตาใกล้กัน
| ข้อสังเกต | สิ่งที่กล่าวได้ | สิ่งที่ยังกล่าวไม่ได้ |
|---|---|---|
| เวลาวัดได้เป็นศูนย์ | ช่วงงานอาจสั้นกว่าที่แยกได้ | ใช้เวลาเป็นศูนย์จริง |
| ช่วงเวลาทับซ้อน | ยังแยกความต่างไม่ชัดในเงื่อนไขนี้ | วิธีหนึ่งเร็วกว่าเสมอ |
| จำนวนเปรียบเทียบน้อยลง | งานชนิดที่นับลดลง | ทุกเครื่องต้องใช้เวลาลดสัดส่วนเดียวกัน |
| คำตอบไม่ครบแต่เร็ว | งานที่ทำต่างจากข้อกำหนด | ปรับประสิทธิภาพสำเร็จ |
ให้ผู้เรียนเขียนรายงานสองฉบับจากตารางสมมติเดียวกัน ฉบับแรกเป็นข้อสรุปที่เกินหลักฐาน แล้วแก้เป็นฉบับที่ระบุขอบเขตถูกต้อง อธิบายคำที่ต้องตัด เช่น “ทุกครั้ง” “ทุกเครื่อง” และ “แน่นอน” หากยังไม่ได้ทดสอบสิ่งที่คำเหล่านั้นครอบคลุม
สุดท้ายกลับไปเลือกวิธีสำหรับข้อมูลสิบรายการบนแพลตฟอร์มรายวิชา โดยให้น้ำหนักความถูกต้อง ความอ่านง่าย และความเหมาะสมกับข้อกำหนดร่วมกับต้นทุน ไม่เพิ่มความซับซ้อนที่อธิบายไม่ได้เพียงเพื่อลดตัวเลขเวลาที่เครื่องมือยังแยกไม่ชัด
ใบงาน 3: สมุดทดลองที่ไม่เลือกเฉพาะผลที่ชอบ
| รายการ | สิ่งที่ใช้จริง |
|---|---|
| คำถามที่ต้องการตอบ | |
| ข้อมูลและจำนวนรายการ | |
| ผลถูกต้องที่ตรวจแล้ว | |
| คำสั่งและตัวเลือกคอมไพล์ | |
| ช่วงงานที่วัดและส่วนที่ไม่นับ | |
| เวลาอย่างน้อยสามครั้งและ checksum | |
| ข้อสรุปที่หลักฐานรองรับ | |
| ข้อจำกัดที่ยังสรุปไม่ได้ |
ลองคอมไพล์ไฟล์เดียวกันแบบ -O0 และ -O2 โดยแยกชื่อไฟล์ผลลัพธ์ แล้วบันทึกตามจริง ไม่สมมติว่าเวลาต้องเรียงลำดับใดเสมอ คอมไพเลอร์อาจปรับหรือเอางานที่ไม่จำเป็นออกได้
หากเวลาที่แสดงเท่ากันหรือช่วงทับซ้อน ให้รายงานว่าเงื่อนไขนี้ยังแยกความต่างไม่ชัด ไม่เพิ่มทศนิยมใน printf แล้วถือว่าได้ความละเอียดทางการวัดเพิ่ม
12.9 อ่านโค้ดที่ผิดและทำความเข้าใจข้อความวินิจฉัย
ตัวอย่างต่อไปนี้เป็นโค้ดฝึกแยกจากงานที่ต้องส่ง แสดงข้อผิดพลาดที่สัมพันธ์กับเนื้อหาบท พร้อมข้อความวินิจฉัยจริง วิธีอ่าน และฉบับแก้ คำว่า diagnostic ใช้รวมข้อความ error, warning และ note จึงไม่ควรเรียกทุกบรรทัดว่า error หรือถือว่าการไม่มี error แปลว่าโปรแกรมถูกตามโจทย์
ที่มาของ log: ผู้เขียนทดสอบด้วย GCC 11.4.0 บน Ubuntu ผ่าน Linux environment เมื่อ 24 ตุลาคม 2569 ใช้ C11 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน
-std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0ข้อความรูปแบบ bad.c:5:17 อ่านว่าไฟล์ bad.c บรรทัด 5 คอลัมน์ 17 ส่วน ^ และ ~ ช่วยชี้บริเวณที่ตรวจพบ จุดนี้อาจเป็นปลายเหตุจากบรรทัดก่อนหน้า error ทำให้การสร้างโปรแกรมล้มในกรณีนั้น warning เตือนความเสี่ยงแม้คำสั่งสร้างอาจจบสำเร็จ และ note ให้บริบท เช่น declaration เดิม ต้องอ่านคู่กัน (GCC: Diagnostics)
ตัวอย่างที่ผิด 1: ใช้ชนิดและค่าคงที่เวลาโดยไม่ include header
ประเภท: ข้อผิดพลาดขณะคอมไพล์
clock_t, clock และ CLOCKS_PER_SEC ประกาศผ่าน time.h โค้ดผิดใช้ชื่อเหล่านี้โดยไม่ include header จึงขาดข้อมูลชนิดและ declaration ฉบับแก้ใช้ ticks สังเคราะห์เพื่อให้ผล conversion คงที่ ไม่อ้างว่าเป็นเวลาที่วัดจากงานจริง
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
int main(void)
{
clock_t start = clock();
printf("%.6f\n", (double)start / CLOCKS_PER_SEC);
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badbad.c: In function 'main':
bad.c:4:5: error: unknown type name 'clock_t'
4 | clock_t start = clock();
| ^~~~~~~
bad.c:4:21: warning: implicit declaration of function 'clock' [-Wimplicit-function-declaration]
4 | clock_t start = clock();
| ^~~~~
bad.c:5:38: error: 'CLOCKS_PER_SEC' undeclared (first use in this function)
5 | printf("%.6f\n", (double)start / CLOCKS_PER_SEC);
| ^~~~~~~~~~~~~~
bad.c:5:38: note: each undeclared identifier is reported only once for each function it appears inสถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 1 คำสั่งล้มและไม่ได้รัน executable ของตัวอย่างนี้ การรันไฟล์เก่าที่อาจค้างจาก build ครั้งก่อนจะไม่ใช่หลักฐานของโค้ดปัจจุบัน
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
unknown type name ชี้ว่าชื่อชนิด clock_t ยังไม่เป็นที่รู้จัก ส่วน implicit declaration ของ clock หรือ undeclared ของ CLOCKS_PER_SEC เป็นข้อความต่อเนื่องที่อาจมีสาเหตุร่วมจาก header ที่ขาด อ่าน error แรกและ note ให้ครบก่อนแก้หลายจุด
โค้ดที่แก้แล้ว
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t ticks = CLOCKS_PER_SEC / 4;
printf("%.6f\n", (double)ticks / CLOCKS_PER_SEC);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
เพิ่ม time.h และทดสอบการแปลงหน่วยด้วย ticks ที่กำหนดไว้ เมื่อจับเวลาจริงต้องตรวจค่า clock ที่อ่านได้และใช้ผลต่างของสองครั้ง ไม่ใช้ค่าจุดเริ่มเพียงค่าเดียวเป็นระยะเวลาของงาน
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
0.250000ตัวอย่างที่ผิด 2: หาร clock ticks แบบจำนวนเต็ม
ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน
บนสภาพแวดล้อมทดสอบ clock_t และ CLOCKS_PER_SEC เป็นชนิดจำนวนเต็ม จึงได้ 0 จากการหาร ticks ที่น้อยกว่าหนึ่งวินาที แล้วค่อยแปลงเป็น double ตัวเลขนี้เป็นข้อมูลสังเคราะห์หนึ่งในสี่วินาที ไม่ใช่ benchmark จริง
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t ticks = CLOCKS_PER_SEC / 4;
double elapsed = ticks / CLOCKS_PER_SEC;
printf("elapsed=%.6f\n", elapsed);
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout:
elapsed=0.000000stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน
สถานะจบที่ตัวเก็บผลอ่านได้: 0
ผลตามข้อกำหนดที่ใช้เทียบ:
elapsed=0.250000อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
ไม่มี compiler error เพราะนิพจน์ถูกตามชนิดที่ระบบทดสอบใช้ ผล 0.000000 ไม่ได้พิสูจน์ว่าอัลกอริทึมใช้เวลาศูนย์เสมอ ต้องแยกการสูญเสียเศษจากความละเอียดนาฬิกาและการวัดที่งานสั้นเกินไป
โค้ดที่แก้แล้ว
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t ticks = CLOCKS_PER_SEC / 4;
double elapsed = (double)ticks / CLOCKS_PER_SEC;
printf("elapsed=%.6f\n", elapsed);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
cast ticks เป็น double ก่อนหารแล้วได้ผลของข้อมูลควบคุม 0.250000 หลังจาก conversion ถูกแล้ว การวัดจริงยังต้องรายงานเครื่อง build flags จำนวนรอบ และข้อจำกัด clock ไม่สรุปว่าเร็วขึ้นจากตัวเลขครั้งเดียว
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
elapsed=0.250000ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 12 การวัดประสิทธิภาพเบื้องต้น
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกวัดประสิทธิภาพด้วยการนับจำนวนรอบการเปรียบเทียบของการค้นหาแบบ linear และ binary ซึ่งให้ผลเป็นค่าคงที่ ส่วนที่สองฝึกจับเวลาการทำงานด้วย clock() และสังเกตว่าค่าเวลาผันแปรในแต่ละครั้ง ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นความต่างระหว่างการวัดที่ให้ค่าคงที่กับการวัดที่ให้ค่าผันแปร
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: นับรอบการเปรียบเทียบ
สถานการณ์ ต้องการวัดว่าการค้นหาแบบ linear กับ binary ใช้จำนวนรอบการเปรียบเทียบต่างกันเพียงใด โดยใส่ตัวนับทั่วไปในฟังก์ชันค้นหาทั้งสอง แล้วค้นหาเลข 30 ในข้อมูลเรียงแล้ว 16 ตัว เพื่อฝึกการวัดประสิทธิภาพด้วยการนับรอบ
เป้าหมาย ฝึก LLO12.1 และ LLO12.3 คือวัดประสิทธิภาพด้วยการนับรอบและตีความผลเพื่อเปรียบเทียบขั้นตอนวิธี
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch12_count.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch12_count.c : นับจำนวนรอบการเปรียบเทียบเพื่อวัดประสิทธิภาพ */
#include <stdio.h>
int linear_count = 0;
int binary_count = 0;
int linear_search(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
linear_count++;
if (arr[i] == target) {
return i;
}
}
return -1;
}
int binary_search(int arr[], int n, int target) {
int low = 0, high = n - 1;
while (low <= high) {
binary_count++;
int mid = low + (high - low) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
int main(void) {
int data[16];
for (int i = 0; i < 16; i++) {
data[i] = i * 2;
}
int target = 30;
linear_search(data, 16, target);
binary_search(data, 16, target);
printf("ค้นหา %d ในข้อมูล 16 ตัว\n", target);
printf("linear search ใช้ %d รอบ\n", linear_count);
printf("binary search ใช้ %d รอบ\n", binary_count);
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch12_count.c -o ch12_count
./ch12_countผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
ค้นหา 30 ในข้อมูล 16 ตัว
linear search ใช้ 16 รอบ
binary search ใช้ 5 รอบฟังก์ชัน linear_search เพิ่มตัวนับ linear_count ทุกครั้งที่เปรียบเทียบสมาชิกหนึ่งตัว เนื่องจากเลข 30 อยู่ท้ายข้อมูลที่ดัชนี 15 จึงตรวจครบทั้ง 16 ตัวก่อนพบ ส่วน binary_search เพิ่ม binary_count ทุกรอบที่ตัดข้อมูลครึ่งหนึ่ง จึงใช้เพียง 5 รอบ ผลการนับนี้เป็นค่าคงที่เหมือนกันทุกเครื่องเพราะนับจากตรรกะของขั้นตอนวิธี ไม่ขึ้นกับความเร็วของฮาร์ดแวร์ ตัวเลขยืนยันเชิงปริมาณว่า binary search ทำงานน้อยรอบกว่าบนข้อมูลเรียงแล้ว
ตัดสินใจเอง A.1 เปลี่ยนขนาดเป็น 32 หรือ 64 ตัว โดยปรับอาร์เรย์และลูปสร้างข้อมูล แล้วเลือกเป้าหมายตัวท้าย 2 * (n - 1) หรือกรณีไม่พบให้สอดคล้องกันก่อนเทียบจำนวน probes พร้อมอธิบายเหตุผล
ผลลัพธ์ที่คาดหวัง A.1 เมื่อเพิ่มขนาดข้อมูล จำนวนรอบของ linear search จะเพิ่มขึ้นตามขนาดข้อมูลในกรณีแย่สุด ขณะที่จำนวนรอบของ binary search เพิ่มขึ้นเพียงเล็กน้อย เพราะตัดข้อมูลครึ่งหนึ่งทุกรอบ (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับขนาดข้อมูลที่แต่ละคนเลือก)
จุดตรวจ A ใส่ตัวนับในฟังก์ชันค้นหาถูกจุด ได้ผล linear 16 รอบ และ binary 5 รอบตามข้อมูลทดสอบ ตีความได้ว่า binary ทำงานน้อยรอบกว่า คอมไพล์ผ่านไม่มีการแจ้งเตือน และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| จำนวนรอบมากหรือน้อยกว่าที่ควร | วางคำสั่งเพิ่มตัวนับผิดจุด เช่น นอกลูปหรือหลังเงื่อนไข | วางคำสั่งเพิ่มตัวนับไว้ต้นลูปหรือต้นรอบการเปรียบเทียบเสมอ |
| ค่าตัวนับไม่ถูกเมื่อวัดซ้ำ | ลืมรีเซ็ตตัวนับเป็นศูนย์ก่อนวัดครั้งใหม่ | ตั้งค่าตัวนับเป็น 0 ก่อนเรียกฟังก์ชันค้นหารอบใหม่ |
| binary search ให้ผลผิด | ข้อมูลยังไม่เรียงลำดับ | ตรวจให้แน่ใจว่าข้อมูลเรียงจากน้อยไปมากก่อนใช้ binary search |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: เป้าหมาย 30 มีอยู่จริง
อาร์เรย์ที่สร้างจาก data[i] = i * 2 เมื่อ n เป็น 16 มีค่า 0, 2, ... , 30 ดังนั้น 30 อยู่ดัชนี 15 การใช้ linear 16 probes ในตัวอย่างเกิดจากพบตัวสุดท้าย ไม่ใช่เพราะไม่มีค่าอยู่ในชุด
| เป้าหมายในชุด 16 ค่า | ดัชนีที่ทำนาย | linear probes | binary probes |
|---|---|---|---|
| 0 | |||
| 14 | |||
| 30 | |||
| 31 |
งาน A.1 เมื่อเปลี่ยนเป็น n เท่ากับ 32 หรือ 64 ให้เลือกเป้าหมายท้ายใหม่ 2 * (n - 1) หรือเป้าหมายที่ไม่มี และระบุกรณีให้ชัด ถ้ายังค้น 30 ในชุด 32 ค่า จะอยู่ตรงกลางครั้งแรกและไม่ได้เป็นกรณีแย่สุด
ถ้าใช้ global counter ให้ทดลองค้น 30 สองครั้งโดยไม่รีเซ็ต แล้วเทียบกับฉบับรีเซ็ตก่อนวัด ไม่เอาจำนวนสะสมไปเรียกว่า probes ต่อหนึ่งคำค้น
Lab B: จับเวลาด้วย clock()
สถานการณ์ ต้องการวัดเวลาการทำงานของลูปขนาดใหญ่ด้วย clock() โดยบวกเลขสะสมสิบล้านรอบ แล้วสังเกตว่าผลรวมเป็นค่าคงที่ แต่ค่าเวลาที่วัดได้ผันแปรในแต่ละครั้ง เพื่อฝึกการจับเวลาและการตีความค่าที่ไม่ตายตัว
เป้าหมาย ฝึก LLO12.2 และ LLO12.3 คือใช้ clock() จับเวลาและอธิบายว่าเหตุใดค่าเวลาจึงอาจเปลี่ยนระหว่างการรัน
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch12_time.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch12_time.c : practice clock timing with a bounded, observable sum */
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t start = clock();
if (start == (clock_t)-1) {
puts("Clock unavailable");
return 1;
}
volatile unsigned long long sum = 0ULL;
for (unsigned long i = 0; i < 10000000UL; ++i) {
sum += i;
}
clock_t end = clock();
if (end == (clock_t)-1 || end < start) {
puts("Clock interval unavailable");
return 1;
}
double seconds = ((double)end - (double)start) / CLOCKS_PER_SEC;
printf("ผลรวม = %llu\n", sum);
printf("clock seconds = %.6f\n", seconds);
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch12_time.c -o ch12_time
./ch12_timeผลคำนวณที่ต้องได้จากโค้ดฉบับนี้ ส่วนค่า clock seconds ให้บันทึกจากการรันของตน ไม่ใช้ตัวเลขเวลาของฉบับก่อนเป็นผลของฉบับใหม่
ผลรวม = 49999995000000
clock seconds = <ค่าที่วัดได้จริง>โปรแกรมบันทึกค่าเวลาเริ่มด้วย clock() ก่อนลูป และค่าเวลาจบหลังลูป แล้วหาผลต่างหารด้วย CLOCKS_PER_SEC เพื่อแปลงเป็นวินาที บรรทัด ผลรวม = 49999995000000 เป็นค่าคงที่เสมอ เพราะคำนวณจากการบวกสะสมที่ให้ผลเท่าเดิมทุกครั้ง ส่วนบรรทัด clock seconds = ... เป็นค่าผันแปร จะอาจเปลี่ยนตามการรัน เครื่อง และ runtime ตามความเร็วของหน่วยประมวลผลและภาระงานอื่นในขณะนั้น ตัวเลขเวลาในรุ่นก่อนจึงไม่ใช่ค่าที่ต้องทำให้เท่ากันในโค้ดฉบับนี้ ต้องวัดใหม่ตาม runtime และตัวเลือกที่ใช้
ตัดสินใจเอง B.1 ต่อยอดโปรแกรมโดยรันซ้ำหลายครั้งติดกัน แล้วบันทึกค่าเวลาที่ได้ในแต่ละครั้ง สังเกตว่าค่าเวลาต่างกันเล็กน้อยทั้งที่ผลรวมเท่าเดิม พร้อมอธิบายว่าเหตุใดจึงเป็นเช่นนั้น
ผลลัพธ์ที่คาดหวัง B.1 ผลรวมจะคงที่เท่ากับ 49999995000000 ทุกครั้ง ขณะที่ค่าเวลาอาจเปลี่ยนหรือแสดงเท่ากันในบางครั้ง เพราะขึ้นกับสภาพของเครื่องและภาระงานอื่นในขณะรัน (ผลที่คาดว่าจะเห็นสำหรับการรันซ้ำบนเครื่องของผู้เรียนเอง ค่าเวลาที่แน่นอนยังไม่ได้ทดสอบในเอกสารนี้ เพราะผันแปรตามเครื่อง)
จุดตรวจ B รวม #include <time.h> ครบ ใช้ clock() และหารด้วย CLOCKS_PER_SEC ถูกต้อง ได้ผลรวมคงที่ 49999995000000 และอธิบายได้ว่าค่าเวลาผันแปรตามเครื่องและปริมาณงาน คอมไพล์ผ่านไม่มีการแจ้งเตือน และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
คอมไพล์ไม่ผ่านที่บรรทัดที่ใช้ clock() | ลืมรวมส่วนหัว #include <time.h> | เพิ่ม #include <time.h> ไว้ส่วนบนของไฟล์ |
| เวลาแสดงเป็น 0 | ช่วงงานอาจสั้นกว่าความละเอียด หรือแปลงหน่วยไม่ถูก | ตรวจสูตรและความละเอียดก่อนสรุปว่าไม่มีต้นทุน |
| เข้าใจว่าค่าเวลาเป็นค่าตายตัว | ยึดค่าเวลาที่วัดได้ครั้งเดียวเป็นตัวเลขแน่นอน | รันหลายครั้งและถือว่าค่าเวลาเป็นค่าโดยประมาณที่ผันแปร |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: ชนิดผลรวมและสถานะนาฬิกา
Lab B ฉบับนี้ใช้ volatile unsigned long long เก็บผลรวม 0 ถึง 9999999 ซึ่งได้ 49999995000000 และอยู่ในช่วงขั้นต่ำของชนิดนี้ ไม่ใช้ long ที่อาจเป็น 32 บิตแล้วเกิด signed overflow บน Windows
| สิ่งที่ตรวจ | ผลที่ต้องการ |
|---|---|
| ผลรวมแต่ละการรัน | เท่ากับค่าคำนวณอิสระ |
| clock เริ่มหรือจบคืน -1 | รายงานใช้เวลาไม่ได้ ไม่นำไปหาร |
| ค่าจบต่ำกว่าค่าเริ่ม | ปฏิเสธช่วงที่ใช้ไม่ได้ |
| เวลาเป็นศูนย์ | ไม่สรุปว่าไม่มีต้นทุน |
| รันซ้ำได้ค่าเดิม | ยอมรับได้ ไม่บังคับให้ต่าง |
| เปิด optimization ต่างกัน | ระบุชื่อไฟล์และตัวเลือกคู่ผล |
volatile ใน Lab นี้ช่วยให้มีงานสะสมที่สังเกตได้ แต่ต้นทุนของ volatile เป็นส่วนหนึ่งของสิ่งที่วัด ไม่ใช้เวลาของ Lab ไปสรุปความเร็วอัลกอริทึมอื่น หรือเทียบ Windows กับ Linux ราวกับ runtime วัดเวลาแบบเดียวกันเสมอ
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch12_count และ ch12_time) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 13
ตอบโดยอ้างหน่วยนับ สัญญาข้อมูล และผลที่บันทึกจริง
- probe กับจำนวนตัวดำเนินการเปรียบเทียบค่าต่างกันอย่างไร
- เหตุใดค้น 30 ในชุด 0 ถึง 30 ทีละสองจึงใช้ linear 16 probes
- ค่า CLOCKS_PER_SEC บอกความละเอียดจริงของนาฬิกาแน่นอนหรือไม่
- ทำไมต้องตรวจผลรวมก่อนเปรียบเทียบเวลา
- การเก็บค่าเฉลี่ยใช้ซ้ำถูกต้องภายใต้เงื่อนไขใด
- เวลาเท่ากันหรือศูนย์หลายครั้งเป็นผลทดลองที่ต้องลบทิ้งหรือไม่
- การปรับโค้ดที่อ่านง่ายขึ้นแต่เวลาแยกไม่ชัดยังมีประโยชน์ด้านใด
เมื่ออธิบายได้ ให้นำเหตุผลการตั้งชื่อ แยกหน้าที่ และหลักฐานก่อน/หลังไปใช้ใน บทที่ 13 มาตรฐานการเขียนโค้ดและการจัดการเวอร์ชัน
สรุป
บทนี้นำการค้นหาจากบทที่ 10 และตัวนับจากบทที่ 11 มาใช้วัดประสิทธิภาพของขั้นตอนวิธีอย่างเป็นรูปธรรม เริ่มจากการวัดด้วยการนับจำนวนรอบการทำงาน โดยใส่ตัวนับทั่วไปในฟังก์ชันค้นหา ซึ่งให้ผลเป็นค่าคงที่เหมือนกันทุกเครื่อง เช่น การค้นหาเลข 30 ในข้อมูลเรียงแล้ว 16 ตัว พบว่า linear search ใช้ 16 รอบ ขณะที่ binary search ใช้เพียง 5 รอบ ต่อด้วยการวัดเวลาจริงด้วย clock() จาก time.h และการแปลงเป็นวินาทีด้วย CLOCKS_PER_SEC โดยเน้นว่าค่าเวลาที่ได้ผันแปรตามเครื่องและปริมาณงาน จึงใช้เปรียบเทียบคร่าว ๆ ไม่ใช่ค่าตายตัว ต่างจากผลรวมที่คงที่ จากนั้นคือหลักคุณภาพโค้ด ได้แก่ การเลี่ยงการคำนวณซ้ำซ้อนด้วยการเก็บผลไว้ในตัวแปร และการสมดุลระหว่างความอ่านง่ายกับประสิทธิภาพ สุดท้ายคือการเลือกวิธีให้เหมาะกับโจทย์ตามขนาดและสภาพของข้อมูลบนผลการวัดจริง ทักษะเหล่านี้เป็นพื้นฐานของมาตรฐานการเขียนโค้ดในบทถัดไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| การวัดประสิทธิภาพด้วยการนับรอบ | ตัวนับทั่วไปเพิ่มค่าทุกการเปรียบเทียบ (ค่าคงที่) | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
| การจับเวลาด้วย clock() | clock(), CLOCKS_PER_SEC, แปลงเป็นวินาที (ค่าผันแปร) | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
| หลักคุณภาพโค้ด | เลี่ยงการคำนวณซ้ำ, สมดุลความอ่านง่ายกับประสิทธิภาพ | บท 13 (มาตรฐานโค้ด), บท 15 (โครงงาน) |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall ch12_count.c -o ch12_count | คอมไพล์ซอร์สโค้ด ch12_count.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch12_time.c -o ch12_time | คอมไพล์ซอร์สโค้ด ch12_time.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
./ch12_count หรือ ./ch12_time | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO12.1) จงอธิบายว่าการวัดประสิทธิภาพด้วยการนับจำนวนรอบต่างจากการจับเวลาจริงอย่างไร และเหตุใดการนับรอบจึงให้ผลเท่ากันทุกเครื่อง
- ก2 (LLO12.2) จงอธิบายหน้าที่ของ
CLOCKS_PER_SECในการคำนวณเวลาด้วยclock()และเหตุใดค่าเวลาที่วัดได้จึงอาจเปลี่ยนระหว่างการรัน
กลุ่ม ข (ประยุกต์)
- ข1 (LLO12.1) จงเขียนโปรแกรมภาษา C ที่มีฟังก์ชันค้นหาแบบ linear พร้อมตัวนับจำนวนรอบ แล้วค้นหาค่าที่ไม่มีอยู่ในอาร์เรย์ขนาด 20 ตัว รายงานจำนวนรอบที่ใช้ แล้วคอมไพล์และรันให้ได้ผลจริง
- ข2 (LLO12.2) จงเขียนโปรแกรมภาษา C ที่ใช้
clock()จับเวลาการทำงานของลูปขนาดใหญ่ แปลงเป็นวินาทีด้วยCLOCKS_PER_SECแล้วรันซ้ำสองครั้งเพื่อสังเกตว่าค่าเวลาต่างกัน พร้อมรวมส่วนหัวที่จำเป็น
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO12.3) จากโปรแกรมที่มีการคำนวณนิพจน์เดิมซ้ำภายในลูป จงวิเคราะห์ว่าควรปรับปรุงอย่างไรให้ลดการคำนวณซ้ำซ้อนโดยยังคงความอ่านง่าย และอธิบายเหตุผล
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาแนวคิดการวัดประสิทธิภาพและการเขียนโปรแกรมที่มีประสิทธิภาพเพิ่มเติมจาก Kernighan และ Ritchie (1988) แล้วทดลองใส่ตัวนับในขั้นตอนวิธีที่เขียนเองเพื่อเปรียบเทียบจำนวนรอบ
- ทบทวนข้อกำหนดของฟังก์ชัน
clock()ส่วนหัวtime.hและค่าคงที่CLOCKS_PER_SECในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจหน่วยของเวลาที่วัดได้และข้อจำกัดของการวัด - ทดลองปรับปรุงโปรแกรมที่มีการคำนวณซ้ำซ้อนภายในลูป แล้วใช้การนับรอบหรือการจับเวลายืนยันว่าการปรับช่วยลดการทำงานได้จริง โดยยังคงความอ่านง่ายของโค้ด
เอกสารอ้างอิง
International Organization for Standardization. (2011). ISO/IEC 9899:2011 — Information technology — Programming languages — C. ISO.
Kernighan, B. W., & Ritchie, D. M. (1988). The C programming language (2nd ed.). Prentice Hall.
ตำราหลักภาษาไทยของรายวิชา. (ผู้สอนเติมรายละเอียดบรรณานุกรม — เพิ่มตามมติผู้สอน 21 ต.ค. 2569).
แหล่งปฐมภูมิที่ตรวจประกอบการปรับปรุง 23 ตุลาคม 2569
ใช้เอกสารข้างต้นตรวจหลักการก่อนเรียบเรียงส่วนขยายและสร้างข้อมูลสมมติ ตัวอย่างใหม่ที่เป็นโปรแกรม C ครบไฟล์ตรวจด้วย GCC 12.2.0 (Debian Linux) และ GCC 6.3.0 (MinGW-w64), C11 และคำเตือน -Wall -Wextra -Wpedantic เมื่อ 23 ตุลาคม 2569 แยกจากผลทดสอบ GCC 13.3.0 ของตัวอย่างเดิม ค่ารูปแบบและผลคำนวณตรวจจากการรัน ส่วนเวลา CPU ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 12
- The Open Group: clock — ความหมายตาม ISO C ค่าที่ใช้ไม่ได้ และการแปลงหน่วย
- Microsoft CRT: clock — พฤติกรรม elapsed wall-clock time ที่ต่างจาก ISO C
- GCC: Optimize Options — ตัวเลือก optimization ที่ต้องบันทึกคู่ผลทดลอง
ตรวจเอกสารก่อนเขียนส่วนเพิ่มเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ C11 และข้อมูลสมมติที่กำหนดขอบเขต การทดสอบผลคำนวณแยกจากเวลาที่สังเกต ไม่สร้างตัวเลข benchmark เพื่อใช้แทนผลการรันจริง