Sign in

Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)

2026-10-22 · updated 2026-10-30 · 22 min read

บทที่ 12 การวัดประสิทธิภาพเบื้องต้นและคุณภาพโค้ด

ShareFacebookXLinkedInLINE

แผนบริหารการสอนประจำบทที่ 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 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท

วิธีสอน

  1. ทบทวนการค้นหาแบบ linear และ binary จากบทที่ 10 และแนวคิดการนับรอบเปรียบเทียบ แล้วตั้งคำถามว่าจะวัดได้อย่างไรว่าวิธีใดทำงานน้อยรอบกว่า เพื่อนำเข้าสู่การวัดประสิทธิภาพด้วยตัวนับ
  2. สาธิตการใส่ตัวนับทั่วไป (global counter) ในฟังก์ชันค้นหาเพื่อนับจำนวนรอบการเปรียบเทียบ และชี้ให้เห็นว่าผลการนับเท่ากันทุกเครื่องเพราะไม่ขึ้นกับความเร็วของฮาร์ดแวร์
  3. สาธิตการจับเวลาด้วย clock() จาก time.h และการแปลงเป็นวินาทีด้วย CLOCKS_PER_SEC พร้อมย้ำว่าค่าเวลาที่ได้ผันแปรตามเครื่องและสภาพการทำงาน จึงใช้เปรียบเทียบคร่าว ๆ ไม่ใช่ค่าตายตัว
  4. อธิบายหลักคุณภาพโค้ด ได้แก่ การเลี่ยงการคำนวณซ้ำซ้อนด้วยการเก็บผลไว้ในตัวแปร และการสมดุลระหว่างความอ่านง่ายกับประสิทธิภาพ โดยไม่ปรับโค้ดให้ซับซ้อนเกินจำเป็น
  5. ให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที

สื่อการเรียนการสอน

  • เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
  • เอกสารประกอบบทที่ 12 และสไลด์สรุปการวัดประสิทธิภาพด้วยการนับรอบ การจับเวลา และหลักคุณภาพโค้ด
  • ไฟล์โปรแกรมตัวอย่าง ch12_count.c และ ch12_time.c

การวัดผลและการประเมินผล

กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 13 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 13 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)

ข้องานหลักฐานผลลัพธ์CLO
1ใส่ตัวนับในฟังก์ชันค้นหาแล้วเทียบจำนวนรอบของ linear กับ binary บนข้อมูลเรียงแล้วไฟล์ซอร์สโค้ด + ผลการรันLLO12.1CLO3
2ใช้ clock() จับเวลาการทำงานของโปรแกรม และอธิบายว่าเหตุใดค่าจึงอาจเปลี่ยนระหว่างการรันไฟล์ซอร์สโค้ด + ผลการรันLLO12.2CLO3
3ปรับโค้ดที่คำนวณซ้ำซ้อนให้ดีขึ้นโดยยังอ่านง่าย พร้อมเหตุผลไฟล์ซอร์สโค้ด + คำอธิบายLLO12.3CLO3

เงื่อนไขผ่าน: งานข้อ 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 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง

เครื่องมือรุ่นที่ทดสอบหน้าที่
คอมไพเลอร์ gcc13.3.0 (Ubuntu)แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ
มาตรฐานภาษาC11 (-std=c11)กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง
⚠️Warning

โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ ผลการนับจำนวนรอบเป็นค่าคงที่เหมือนกันทุกเครื่อง แต่ค่าเวลาที่ได้จาก clock() ผันแปรตามเครื่องและสภาพการทำงาน จึงไม่ใช่ค่าตายตัว หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ค่าเวลาที่แสดงอาจต่างออกไป แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน

วิธีใช้บทเรียนและใบงานในหน้านี้

ทบทวน บทที่ 11 การดีบักและการทดสอบระดับหน่วย โดยเก็บชุดทดสอบที่ผ่านไว้ก่อนปรับโค้ด การวัดวิธีที่ตอบผิดไม่ได้ตอบว่าวิธีใดทำงานเดียวกันได้เร็วกว่า

ใบงานเป็นตารางสำหรับคัดลงสมุดหรือเอกสารของตน ไม่ใช่ช่องส่งคำตอบบนเว็บไซต์ ใช้ลำดับ กำหนดงาน → ตรวจผล → ระบุหน่วยและช่วงวัด → รันซ้ำ → รายงานข้อจำกัด ไม่คัดลอกเวลาตัวอย่างเป็นผลของตน

ข้อมูลเป็นข้อมูลสมมติ กิจกรรมเพิ่มไม่เปลี่ยน LLO12.1–12.3 งานส่ง หรือสัดส่วนคะแนน แผนเวลาและแนวคำตอบอยู่เฉพาะส่วนผู้สอน เวลาแผนสอนไม่ใช่เวลา CPU ของโปรแกรมและไม่ใช่ผลวัดการสอนจริง

12.1 การวัดประสิทธิภาพด้วยการนับรอบ

ระดับที่ 1: การวัดประสิทธิภาพด้วยการนับรอบคือการนับจำนวนครั้งที่โปรแกรมทำงานสำคัญ เช่น จำนวนครั้งที่เปรียบเทียบค่า เพื่อประเมินว่าขั้นตอนวิธีหนึ่งทำงานมากหรือน้อยเพียงใด

ระดับที่ 2: วิธีที่ใช้ได้ทันทีคือประกาศตัวแปรนับทั่วไป (global counter) แล้วเพิ่มค่าตัวนับหนึ่งครั้งทุกครั้งที่เกิดการทำงานที่ต้องการวัด เช่น วางคำสั่ง count++; ไว้ต้นลูปหรือต้นฟังก์ชันค้นหา เมื่อโปรแกรมทำงานเสร็จ ค่าของตัวนับคือจำนวนรอบทั้งหมด ข้อดีของการนับรอบคือผลซ้ำได้สำหรับโค้ด ข้อมูล หน่วย และสถานะเริ่มเดียวกัน เพราะนับจากตรรกะของขั้นตอนวิธี ไม่ได้ขึ้นกับความเร็วของฮาร์ดแวร์หรือสภาพการทำงานของเครื่อง การนับรอบต่อยอดจากการใช้ตัวนับในบทที่ 5 และบทที่ 11 โดยตรง

C
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 ที่ผู้เรียกอาจลืมรีเซ็ต

C
/* 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 บอกจำนวนหน่วยนับเวลาต่อหนึ่งวินาที จึงใช้แปลงผลต่างของตัวนับเวลาให้เป็นวินาทีได้ถูกต้อง

C
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: เทคนิคพื้นฐานหนึ่งคือการเลี่ยงการคำนวณซ้ำซ้อน หากนิพจน์ใดให้ผลเท่าเดิมทุกรอบของลูป ควรคำนวณครั้งเดียวแล้วเก็บผลไว้ในตัวแปรก่อนลูป แทนการคำนวณใหม่ซ้ำ ๆ ภายในลูป ตัวอย่างเช่น หากขอบเขตของลูปต้องเรียกฟังก์ชันที่ให้ค่าคงที่ ควรเก็บค่านั้นไว้ในตัวแปรก่อน แล้วใช้ตัวแปรเป็นเงื่อนไขของลูป วิธีนี้ลดจำนวนการทำงานลงโดยไม่เปลี่ยนผลลัพธ์

C
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 และรับเป้าหมายหนึ่งค่า โปรแกรมนับการเปรียบเทียบค่าข้อมูลด้วย == หรือ < อย่างละหนึ่งครั้ง ไม่รวมเงื่อนไขลูปหรือการสลับ เพื่อให้หน่วยที่นำมาบวกเหมือนกัน

C
/* 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: มากที่สุดเมื่อค้นหนึ่งครั้ง
5105
104510
2019020

ถ้าต้องเรียง 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 เท่ากับหนึ่งมิลลิวินาทีในทุกระบบ

C
#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 เท่ากันก่อนนำเวลามาอภิปราย

C
/* 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 เปรียบเทียบส่วนต่างจากค่าเฉลี่ยแบบคำนวณซ้ำกับแบบเก็บค่าเฉลี่ย โดยตรวจว่าผลของแต่ละสมาชิกยังเท่ากัน

C
/* 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 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน

TEXT
-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 คงที่ ไม่อ้างว่าเป็นเวลาที่วัดจากงานจริง

โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้

C
#include <stdio.h>
int main(void)
{
    clock_t start = clock();
    printf("%.6f\n", (double)start / CLOCKS_PER_SEC);
    return 0;
}

คำสั่งสร้างและข้อความที่ได้จริง

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad
TEXT
bad.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 ให้ครบก่อนแก้หลายจุด

โค้ดที่แก้แล้ว

C
#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 ว่าง

TEXT
0.250000

ตัวอย่างที่ผิด 2: หาร clock ticks แบบจำนวนเต็ม

ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน

บนสภาพแวดล้อมทดสอบ clock_t และ CLOCKS_PER_SEC เป็นชนิดจำนวนเต็ม จึงได้ 0 จากการหาร ticks ที่น้อยกว่าหนึ่งวินาที แล้วค่อยแปลงเป็น double ตัวเลขนี้เป็นข้อมูลสังเคราะห์หนึ่งในสี่วินาที ไม่ใช่ benchmark จริง

โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้

C
#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;
}

คำสั่งสร้างและข้อความที่ได้จริง

TEXT
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o bad

ไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม

สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น

ผลจากการรันฉบับผิด

stdout:

TEXT
elapsed=0.000000

stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน

สถานะจบที่ตัวเก็บผลอ่านได้: 0

ผลตามข้อกำหนดที่ใช้เทียบ:

TEXT
elapsed=0.250000

อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร

ไม่มี compiler error เพราะนิพจน์ถูกตามชนิดที่ระบบทดสอบใช้ ผล 0.000000 ไม่ได้พิสูจน์ว่าอัลกอริทึมใช้เวลาศูนย์เสมอ ต้องแยกการสูญเสียเศษจากความละเอียดนาฬิกาและการวัดที่งานสั้นเกินไป

โค้ดที่แก้แล้ว

C
#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 ว่าง

TEXT
elapsed=0.250000

ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้

  1. ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
  2. อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
  3. แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
  4. ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน

ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)

ปฏิบัติการที่ 12 การวัดประสิทธิภาพเบื้องต้น

ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกวัดประสิทธิภาพด้วยการนับจำนวนรอบการเปรียบเทียบของการค้นหาแบบ linear และ binary ซึ่งให้ผลเป็นค่าคงที่ ส่วนที่สองฝึกจับเวลาการทำงานด้วย clock() และสังเกตว่าค่าเวลาผันแปรในแต่ละครั้ง ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นความต่างระหว่างการวัดที่ให้ค่าคงที่กับการวัดที่ให้ค่าผันแปร

ℹ️Note

ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0

Lab A: นับรอบการเปรียบเทียบ

สถานการณ์ ต้องการวัดว่าการค้นหาแบบ linear กับ binary ใช้จำนวนรอบการเปรียบเทียบต่างกันเพียงใด โดยใส่ตัวนับทั่วไปในฟังก์ชันค้นหาทั้งสอง แล้วค้นหาเลข 30 ในข้อมูลเรียงแล้ว 16 ตัว เพื่อฝึกการวัดประสิทธิภาพด้วยการนับรอบ

เป้าหมาย ฝึก LLO12.1 และ LLO12.3 คือวัดประสิทธิภาพด้วยการนับรอบและตีความผลเพื่อเปรียบเทียบขั้นตอนวิธี

ขั้นปฏิบัติ

  1. สร้างไฟล์ชื่อ ch12_count.c ด้วยโปรแกรมแก้ไขข้อความหรือ IDE
  2. พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
C
/* 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;
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch12_count.c -o ch12_count
./ch12_count

ผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)

TEXT
ค้นหา 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 probesbinary 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() จับเวลาและอธิบายว่าเหตุใดค่าเวลาจึงอาจเปลี่ยนระหว่างการรัน

ขั้นปฏิบัติ

  1. สร้างไฟล์ชื่อ ch12_time.c ด้วยโปรแกรมแก้ไขข้อความหรือ IDE
  2. พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
C
/* 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;
}
  1. คอมไพล์และรันด้วยคำสั่ง
BASH
gcc -std=c11 -Wall ch12_time.c -o ch12_time
./ch12_time

ผลคำนวณที่ต้องได้จากโค้ดฉบับนี้ ส่วนค่า clock seconds ให้บันทึกจากการรันของตน ไม่ใช้ตัวเลขเวลาของฉบับก่อนเป็นผลของฉบับใหม่

TEXT
ผลรวม = 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

ตอบโดยอ้างหน่วยนับ สัญญาข้อมูล และผลที่บันทึกจริง

  1. probe กับจำนวนตัวดำเนินการเปรียบเทียบค่าต่างกันอย่างไร
  2. เหตุใดค้น 30 ในชุด 0 ถึง 30 ทีละสองจึงใช้ linear 16 probes
  3. ค่า CLOCKS_PER_SEC บอกความละเอียดจริงของนาฬิกาแน่นอนหรือไม่
  4. ทำไมต้องตรวจผลรวมก่อนเปรียบเทียบเวลา
  5. การเก็บค่าเฉลี่ยใช้ซ้ำถูกต้องภายใต้เงื่อนไขใด
  6. เวลาเท่ากันหรือศูนย์หลายครั้งเป็นผลทดลองที่ต้องลบทิ้งหรือไม่
  7. การปรับโค้ดที่อ่านง่ายขึ้นแต่เวลาแยกไม่ชัดยังมีประโยชน์ด้านใด

เมื่ออธิบายได้ ให้นำเหตุผลการตั้งชื่อ แยกหน้าที่ และหลักฐานก่อน/หลังไปใช้ใน บทที่ 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) จากโปรแกรมที่มีการคำนวณนิพจน์เดิมซ้ำภายในลูป จงวิเคราะห์ว่าควรปรับปรุงอย่างไรให้ลดการคำนวณซ้ำซ้อนโดยยังคงความอ่านง่าย และอธิบายเหตุผล

การศึกษาค้นคว้าเพิ่มเติม

  1. ศึกษาแนวคิดการวัดประสิทธิภาพและการเขียนโปรแกรมที่มีประสิทธิภาพเพิ่มเติมจาก Kernighan และ Ritchie (1988) แล้วทดลองใส่ตัวนับในขั้นตอนวิธีที่เขียนเองเพื่อเปรียบเทียบจำนวนรอบ
  2. ทบทวนข้อกำหนดของฟังก์ชัน clock() ส่วนหัว time.h และค่าคงที่ CLOCKS_PER_SEC ในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจหน่วยของเวลาที่วัดได้และข้อจำกัดของการวัด
  3. ทดลองปรับปรุงโปรแกรมที่มีการคำนวณซ้ำซ้อนภายในลูป แล้วใช้การนับรอบหรือการจับเวลายืนยันว่าการปรับช่วยลดการทำงานได้จริง โดยยังคงความอ่านง่ายของโค้ด

เอกสารอ้างอิง

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 เพื่อใช้แทนผลการรันจริง

By Watcharin Sarachai