Undefined behavior (1)

undefined behavior เป็นเรื่องอิสระภาพของ compiler ครับ
เป็นเรื่องที่ compiler มีอิสระ จะทำอย่างไรก็ได้ และไม่จำเป็นต้องระบุด้วยครับ
แตกต่างจาก implementation-defined ซึ่ง compiler ต้องระบุให้ชัดเจนว่าจะทำอย่างไร

undefined behavior ที่น่าจะเป็นปัญหามากที่สุดคือ order of evaluation ครับ
เพราะจะทำให้ผิดง่าย หาที่ผิดยาก ประมาณว่าถึงรู้ว่ามีที่ผิด ถ้าต้องแก้ให้ถูกทั้งหมดนี่เรื่องใหญ่เลย

ปัญหาจาก order of evaluation เรื่องแรกที่จะยกตัวอย่างคือ
การส่ง parameter ขณะเรียกใช้ function
ลองดู code ตัวอย่าง

#include <stdio.h>
int main() {
int i=0;
printf("%d,%d\n",++i,++i);
return 0;
}

code ข้างบนถ้าไป compile และรันด้วย
Microsoft VC++ บน Windows CPU intel
GNU gcc บน Linux CPU intel
จะได้ผลลัพธ์เป็น
2,1
แต่ถ้าไป compile และรันด้วย
GNU gcc บน Solaris CPU SPARC
ก็จะได้ผลลัพธ์เป็น
1,2
จะเห็นได้ว่า ได้ผลลัพธ์ไม่เหมือนกันครับ

statement ที่เป็นปัญหาคือ
printf("%d,%d\n",++i,++i);
ใน statement นี้เป็นการเรียก function printf โดยมี
"%d,%d\n" เป็น parameter ตัวแรก
++i เป็น parameter ตัวที่สอง
++i เป็น parameter ตัวที่สาม
ในการส่ง parameter ไปให้ function นั้น compiler มีอิสระที่จะ evaluate ค่าที่จะส่ง
โดยทำจาก ซ้ายไปขวา หรือ ขวาไปซ้าย ก็ได้

ลองมาดูว่าเกิดอะไรขึ้น
ก่อนหน้า statement นี้ i จะมีค่าเป็น 0

ถ้า compiler เลือกที่จะ evaluate ค่าที่จะส่งไปให้ printf จาก ซ้ายไปขวา
parameter ตัวแรกจะเป็น "%d,%d\n" ซึ่งเป็นค่าคงที่แบบ string
parameter ตัวที่สองจะเป็น 1 เพราะ i เป็น 0 อยู่ พอทำ ++i เลยได้ค่าเป็น 1 และ i กลายเป็น 1
parameter ตัวที่สามจะเป็น 2 เพราะ i เป็น 1 อยู่ พอทำ ++i เลยได้ค่าเป็น 2 และ i กลายเป็น 2
เลยได้ผลประมาณว่าส่ง 1 กับ 2 ไปให้ printf ทำ "%d,%d\n"
printf("%d,%d\n",1,2);
ผลออกมาเลยเป็น
1,2

ถ้า compiler เลือกที่จะ evaluate ค่าที่จะส่งไปให้ printf จาก ขวาไปซ้าย
parameter ตัวที่สามจะเป็น 1 เพราะ i เป็น 0 อยู่ พอทำ ++i เลยได้ค่าเป็น 1 และ i กลายเป็น 1
parameter ตัวที่สองจะเป็น 2 เพราะ i เป็น 1 อยู่ พอทำ ++i เลยได้ค่าเป็น 2 และ i กลายเป็น 2
parameter ตัวแรกจะเป็น "%d,%d\n" ซึ่งเป็นค่าคงที่แบบ string
เลยได้ผลประมาณว่าส่ง 2 กับ 1 ไปให้ printf ทำ "%d,%d\n"
printf("%d,%d\n",2,1);
ผลออกมาเลยเป็น
2,1

เห็นมั๊ยครับว่า ส่งจากซ้ายไปขวา กับ ขวาไปซ้าย จะให้ผลไม่เหมือนกัน
นี่คือสาเหตุที่ทำให้ printf ออกมาไม่เหมือนกันครับ

อีกตัวอย่างหนึ่งของปัญหาจาก order of evaluation คือการ assign ค่าครับ
ลองดู code นี้
int a[10], i=0;
a[i]=i++;

statement ที่เป็นปัญหาคือ
a[i]=i++;
อันนี้จะเป็นปัญหาของการที่ compiler ทำการ evaluate ข้างซ้าย หรือ ข้างขวา ของเครื่องหมาย =
ว่าจะทำข้างไหนก่อน

ลองมาดูว่าเกิดอะไรขึ้น
ก่อนหน้า statement นี้ i จะมีค่าเป็น 0

ถ้า compiler เลือกที่จะ evaluate ข้างซ้ายก่อน
ข้างซ้ายคือ a[i] และ i เป็น 0 อยู่
ข้างซ้ายก็จะกลายเป็น a[0]
ส่วนข้างขวาคือ i++ จะได้ค่าเป็น 0 เพราะ i เป็น 0 ก่อนจะทำ i++ ให้ i กลายเป็น 1
จับข้างซ้ายมา assign กับข้างขวา ผลคือ
a[0]=0
คือเอาค่า 0 ไปใส่ที่ a[0]

ถ้า compiler เลือกที่จะ evaluate ข้างขวาก่อน
ข้างขวาคือ i++ จะได้ค่าเป็น 0 เพราะ i เป็น 0 ก่อนจะทำ i++ ให้ i กลายเป็น 1
ส่วนข้างซ้ายคือ a[i] และ i กลายเป็น 1 ไปแล้ว
ข้างซ้ายก็จะกลายเป็น a[1]
จับข้างซ้ายมา assign กับข้างขวา ผลคือ
a[1]=0
คือเอาค่า 0 ไปใส่ที่ a[1]

เห็นผลที่แตกต่างกันหรือยังครับ
คงพอนึกออกนะครับว่า source code เดียวกัน
แต่ไป compile และรันในแต่ละ compiler แล้วอาจได้ผลลัพธ์ไม่เหมือนกัน

แล้วเราจะหลีกเลี่ยงปัญหาจาก order of evaluation ได้ยังไง
ถ้าดูจากตัวอย่างข้างบน
statement ที่เป็นปัญหาคือ
printf("%d,%d\n",++i,++i);
กับ
a[i]=i++;
จะมีลักษณะที่เหมือนกันอย่างหนึ่ง คือ
ภายใน statement เดียวกัน
มีการ ใช้ค่า และ เปลี่ยนค่า ของ variable ตัวเดียวกัน มากกว่าหนึ่งครั้ง
ทำให้เกิดทางเลือก ใช้ก่อนเปลี่ยน หรือ เปลี่ยนก่อนใช้
หรือเปลี่ยนค่าหลายครั้ง เลยมีทางเลือกว่าจะเอาค่าไหนมาใช้
เปิดช่องทางเลือกหลายทางให้ compiler นั่นเอง

ทางแก้คืออย่าปล่อยให้มีทางเลือกครับ
ให้แตก statement ออกเป็นหลาย statement แทน
statement ที่ใช้ค่าของ variable ไหนก็จะไม่มีการเปลี่ยนค่าของ variable นั้น
หรือ statement ที่มีการเปลี่ยนค่าของ variable ก็จะไม่ใช้ค่าของ variable นั้น
เช่น
printf("%d,%d\n",++i,++i);
เปลี่ยนเป็น
printf("%d,%d\n",i+1,i+2); //ไม่มีการเปลี่ยนค่า i
i+=2; // เปลี่ยนค่า i
และ
a[i]=i++;
เปลี่ยนเป็น
a[i]=i; //ไม่มีการเปลี่ยนค่า i
i++; // เปลี่ยนค่า i
ก็จะไม่เกิดปัญหาจาก order of evaluation

ประมาณว่าเขียน statement แบบง่ายๆ ไม่ซับซ้อน จะมีปัญหาน้อยกว่าครับ

ตัวอย่างที่ยกมา เป็นแบบที่เห็นปัญหาได้ง่าย การเปลี่ยนค่าของ variable
ทำแบบง่ายๆ เห็นได้ชัดเจนว่าคือ ++i หรือ i++
แต่ในการทำงานจริง การเปลี่ยนค่าของ variable หรือ state จะซับซ้อนกว่านี้มาก
ตัวอย่างเช่น int fgetc(FILE *fp) เป็น function ที่ใช้อ่านค่าหนึ่ง byte จาก file
โดยมี FILE *fp เป็น file pointer
การเรียกใช้ fgetc จะมีการเปลี่ยนค่า variable หรือ state ภายในของ fp ด้วย ดังนั้น
printf ("%d,%d\n",fgetc(fp),fgetc(fp));
ก็จะได้ผลไม่เหมือนกัน เวลาเปลี่ยน compiler หรือ platform
การแก้ก็ต้องทำ statement ให้ย่อยลง ให้เปลี่ยนค่าแค่ครั้งเดียว เช่น
printf("%d,",fgetc(fp));
printf("%d\n",fgetc(fp));

เห็นมั๊ยครับว่า หาที่ผิดได้ยากขนาดไหน

แล้วทำไม standard C ถึงไม่กำหนดไปเลยว่าให้ compiler ทำให้เหมือนกัน
เหตุผลก็คือว่า order of evaluation เป็นเรื่องของ compiler กับ architecture ของ CPU ครับ
เพื่อให้ได้ code ที่มีประสิทธิภาพสูง ทำงานเร็ว เลยต้องปล่อยให้ compiler เป็นผู้เลือกว่าจะทำยังไง

สรุปว่าถ้าอยากให้โปรแกรมทำงานเร็ว ก็ต้องเขียนโปรแกรมยากๆหน่อย
speed does matter จริงๆ





Create Date : 27 สิงหาคม 2551
Last Update : 27 สิงหาคม 2551 21:19:34 น.
Counter : 740 Pageviews.

0 comments
ชื่อ :
Comment :
 *ใช้ code html ตกแต่งข้อความได้เฉพาะสมาชิก
 

Zkaru.BlogGang.com

zkaru
Location :
กรุงเทพฯ  Thailand

[ดู Profile ทั้งหมด]
 ผู้ติดตามบล็อก : 2 คน [?]