(A + B + C) ≠ (A + C + B) และการเรียงลำดับคอมไพเลอร์


108

การเพิ่มจำนวนเต็ม 32 บิตสองตัวอาจทำให้จำนวนเต็มล้น:

uint64_t u64_z = u32_x + u32_y;

การโอเวอร์โฟลว์นี้สามารถหลีกเลี่ยงได้หากหนึ่งในจำนวนเต็ม 32 บิตถูกแคสต์หรือเพิ่มเป็นจำนวนเต็ม 64 บิตก่อน

uint64_t u64_z = u32_x + u64_a + u32_y;

อย่างไรก็ตามหากคอมไพเลอร์ตัดสินใจที่จะจัดลำดับการเพิ่มใหม่:

uint64_t u64_z = u32_x + u32_y + u64_a;

การล้นจำนวนเต็มอาจยังคงเกิดขึ้น

คอมไพเลอร์ได้รับอนุญาตให้ทำการเรียงลำดับใหม่หรือเราสามารถไว้วางใจให้คอมไพเลอร์สังเกตเห็นความไม่สอดคล้องกันของผลลัพธ์และรักษาลำดับนิพจน์ตามที่เป็นอยู่ได้หรือไม่?


15
คุณไม่ได้แสดงจำนวนเต็มuint32_tมากเกินไปเพราะดูเหมือนว่าคุณจะถูกเพิ่ม- ซึ่งจะไม่ล้นเกิน สิ่งเหล่านี้ไม่ใช่พฤติกรรมที่แตกต่างกัน
Martin Bonner สนับสนุน Monica

5
ดูส่วน 1.9 ของมาตรฐาน c ++ ซึ่งตอบคำถามของคุณได้โดยตรง (มีตัวอย่างที่เกือบจะเหมือนกับของคุณ)
Holt

3
@Tal: ตามที่คนอื่นระบุไว้แล้ว: ไม่มีจำนวนเต็มล้น Unsigned ถูกกำหนดให้ห่อสำหรับการลงนามมันเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นการใช้งานใด ๆ จะทำรวมถึงจมูก daemons
ซื่อสัตย์เกินไปสำหรับเว็บไซต์นี้

5
@Tal: ไร้สาระ! ตามที่ฉันได้เขียนไปแล้ว: มาตรฐานมีความชัดเจนมากและต้องมีการห่อไม่ทำให้อิ่มตัว (ซึ่งอาจเป็นไปได้เมื่อมีการลงนามเนื่องจากเป็น UB ตามมาตรฐาน
ซื่อสัตย์เกินไปสำหรับไซต์นี้

15
@rustyx: ไม่ว่าคุณจะเรียกมันว่าการห่อหรือล้นจุดก็ยังคงอยู่ที่((uint32_t)-1 + (uint32_t)1) + (uint64_t)0ผลลัพธ์ใน0ขณะที่(uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)ผลลัพธ์เป็น0x100000000และค่าทั้งสองนี้ไม่เท่ากัน ดังนั้นจึงเป็นเรื่องสำคัญที่คอมไพเลอร์สามารถใช้การแปลงนั้นได้หรือไม่ แต่ใช่มาตรฐานใช้คำว่า "ล้น" สำหรับจำนวนเต็มที่มีการลงนามเท่านั้นไม่ใช่ไม่ได้ลงนาม
Steve Jessop

คำตอบ:


84

หากเครื่องมือเพิ่มประสิทธิภาพทำการจัดลำดับใหม่ดังกล่าวจะยังคงผูกพันกับข้อกำหนด C ดังนั้นการจัดลำดับใหม่จะกลายเป็น:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

เหตุผล:

เริ่มต้นด้วย

uint64_t u64_z = u32_x + u64_a + u32_y;

การเพิ่มจะดำเนินการจากซ้ายไปขวา

รัฐกฎการส่งเสริมจำนวนเต็มว่าในนอกจากนี้เป็นครั้งแรกในการแสดงออกของเดิมได้รับการเลื่อนตำแหน่งให้เป็นu32_x uint64_tนอกจากนี้ที่สองก็จะได้รับการเลื่อนตำแหน่งให้u32_yuint64_t

ดังนั้นเพื่อให้เป็นไปตามข้อกำหนด C ผู้เพิ่มประสิทธิภาพใด ๆ ต้องเลื่อนระดับu32_xและu32_yเป็นค่าที่ไม่ได้ลงนาม 64 บิต นี่เท่ากับการเพิ่มนักแสดง (การปรับให้เหมาะสมจริงไม่ได้ทำที่ระดับ C แต่ฉันใช้รูปแบบ C เพราะนั่นเป็นสัญกรณ์ที่เราเข้าใจ)


มันไม่ได้เชื่อมโยงทางซ้าย(u32_x + u32_t) + u64_aเหรอ?
ไร้ประโยชน์

12
@Useless: Klas โยนทุกอย่างเป็น 64 บิต ตอนนี้คำสั่งไม่ได้สร้างความแตกต่างเลย คอมไพเลอร์ไม่จำเป็นต้องทำตามการเชื่อมโยง แต่จะต้องให้ผลลัพธ์เดียวกันกับที่มันทำ
gnasher729

2
ดูเหมือนจะแนะนำว่ารหัสของ OP จะได้รับการประเมินแบบนั้นซึ่งไม่เป็นความจริง
ไร้ประโยชน์

@Klas - การดูแลที่จะอธิบายว่าทำไมเป็นกรณีนี้และวิธีการว่าคุณมาถึงที่ตัวอย่างโค้ดของคุณหรือไม่
rustyx

1
@rustyx มันต้องการคำอธิบาย ขอบคุณที่ผลักดันให้ฉันเพิ่ม
Klas Lindbäck

28

คอมไพเลอร์ได้รับอนุญาตให้สั่งซื้อใหม่ภายใต้กฎas ifเท่านั้น นั่นคือหากการเรียงลำดับใหม่จะให้ผลลัพธ์เหมือนกับลำดับที่ระบุไว้เสมอก็จะได้รับอนุญาต มิฉะนั้น (ตามตัวอย่างของคุณ) ไม่ใช่

ตัวอย่างเช่นได้รับนิพจน์ต่อไปนี้

i32big1 - i32big2 + i32small

ซึ่งได้รับการสร้างขึ้นอย่างรอบคอบเพื่อลบค่าสองค่าซึ่งเป็นที่ทราบกันดีว่ามีค่ามาก แต่ใกล้เคียงกันจากนั้นจึงเพิ่มค่าอื่น ๆ เล็กน้อย (เพื่อหลีกเลี่ยงการล้น) คอมไพเลอร์อาจเลือกที่จะเรียงลำดับใหม่เป็น:

(i32small - i32big2) + i32big1

และอาศัยข้อเท็จจริงที่ว่าแพลตฟอร์มเป้าหมายใช้เลขคณิตสองส่วนร่วมกับการตัดรอบเพื่อป้องกันปัญหา (การเรียงลำดับใหม่ดังกล่าวอาจสมเหตุสมผลหากคอมไพเลอร์ถูกกดเพื่อรีจิสเตอร์และมีi32smallในรีจิสเตอร์อยู่แล้ว)


ตัวอย่างของ OP ใช้ประเภทที่ไม่ได้ลงชื่อ i32big1 - i32big2 + i32smallหมายถึงประเภทที่ลงนาม ความกังวลเพิ่มเติมเข้ามามีบทบาท
chux - คืนสถานะ Monica

@chux อย่างแน่นอน. ประเด็นที่ฉันพยายามทำคือแม้ว่าฉันจะไม่สามารถเขียนได้(i32small-i32big2) + i32big1(เพราะอาจทำให้เกิด UB) คอมไพลเลอร์สามารถจัดเรียงใหม่เพื่อให้มีประสิทธิภาพเนื่องจากคอมไพลเลอร์สามารถมั่นใจได้ว่าพฤติกรรมจะถูกต้อง
Martin Bonner สนับสนุน Monica

3
@chux: ข้อกังวลเพิ่มเติมเช่น UB ไม่เข้ามามีบทบาทเพราะเรากำลังพูดถึงคอมไพเลอร์ที่เรียงลำดับใหม่ภายใต้กฎ as-if คอมไพเลอร์เฉพาะอาจใช้ประโยชน์จากการรู้พฤติกรรมล้นของตัวเอง
MSalters

16

มีกฎ "เสมือน" ใน C, C ++ และ Objective-C: คอมไพลเลอร์สามารถทำอะไรก็ได้ตราบเท่าที่ไม่มีโปรแกรมที่สอดคล้องกันสามารถบอกความแตกต่างได้

ในภาษาเหล่านี้ a + b + c ถูกกำหนดให้เหมือนกับ (a + b) + c หากคุณสามารถบอกความแตกต่างระหว่างสิ่งนี้กับตัวอย่าง a + (b + c) คอมไพลเลอร์จะไม่สามารถเปลี่ยนลำดับได้ หากคุณไม่สามารถบอกความแตกต่างได้แสดงว่าคอมไพเลอร์สามารถเปลี่ยนลำดับได้ฟรี แต่ไม่เป็นไรเพราะคุณไม่สามารถบอกความแตกต่างได้

ในตัวอย่างของคุณด้วย b = 64 บิต a และ c 32 บิตคอมไพเลอร์จะได้รับอนุญาตให้ประเมิน (b + a) + c หรือแม้แต่ (b + c) + a เพราะคุณไม่สามารถบอกความแตกต่างได้ แต่ ไม่ใช่ (a + c) + b เพราะคุณสามารถบอกความแตกต่างได้

กล่าวอีกนัยหนึ่งคอมไพลเลอร์ไม่ได้รับอนุญาตให้ทำสิ่งใด ๆ ที่ทำให้โค้ดของคุณทำงานแตกต่างจากที่ควร ไม่จำเป็นต้องสร้างรหัสที่คุณคิดว่าจะสร้างหรือที่คุณคิดว่าควรสร้างขึ้น แต่รหัสจะให้ผลลัพธ์ที่ตรงกับที่ควร


แต่มีข้อแม้ใหญ่ คอมไพเลอร์มีอิสระที่จะถือว่าไม่มีพฤติกรรมที่ไม่ได้กำหนดไว้ (ในกรณีนี้คือโอเวอร์โฟลว์) ซึ่งคล้ายกับวิธีที่if (a + 1 < a)สามารถปรับให้เหมาะสมกับการตรวจสอบล้น
csiz

7
@csiz ... บนตัวแปรที่ลงนาม ตัวแปรที่ไม่ได้ลงนามมีความหมายล้นที่กำหนดไว้อย่างดี (ล้อมรอบ)
Gavin S. Yancey

7

อ้างอิงจากมาตรฐาน :

[หมายเหตุ: ตัวดำเนินการสามารถจัดกลุ่มใหม่ได้ตามกฎทางคณิตศาสตร์ตามปกติเฉพาะในกรณีที่ตัวดำเนินการเชื่อมโยงกันหรือสับเปลี่ยนเท่านั้นตัวอย่างเช่นในส่วนต่อไปนี้ int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

คำสั่งนิพจน์ทำงานเหมือนกับ

a = (((a + 32760) + b) + 5);

เนื่องจากความเชื่อมโยงและความสำคัญของตัวดำเนินการเหล่านี้ ดังนั้นผลลัพธ์ของผลรวม (a + 32760) จะถูกเพิ่มเข้าไปใน b ถัดไปและผลลัพธ์นั้นจะถูกเพิ่มเป็น 5 ซึ่งส่งผลให้ค่าที่กำหนดให้กับ a บนเครื่องที่โอเวอร์โฟลว์ทำให้เกิดข้อยกเว้นและช่วงของค่าที่แสดงได้โดย int คือ [-32768, + 32767] การนำไปใช้ไม่สามารถเขียนนิพจน์นี้ใหม่เป็น

a = ((a + b) + 32765);

เนื่องจากถ้าค่าของ a และ b เป็นตามลำดับ -32754 และ -15 ผลรวม a + b จะทำให้เกิดข้อยกเว้นในขณะที่นิพจน์ดั้งเดิมจะไม่ และไม่สามารถเขียนนิพจน์ใหม่เป็น

a = ((a + 32765) + b);

หรือ

a = (a + (b + 32765));

เนื่องจากค่าของ a และ b อาจเป็น 4 และ -8 หรือ -17 และ 12 ตามลำดับอย่างไรก็ตามในเครื่องที่การล้นไม่ก่อให้เกิดข้อยกเว้นและผลลัพธ์ของการล้นจะย้อนกลับได้คำสั่งนิพจน์ข้างต้นสามารถ ถูกเขียนใหม่โดยการใช้งานด้วยวิธีการใด ๆ ข้างต้นเนื่องจากผลลัพธ์เดียวกันจะเกิดขึ้น - หมายเหตุ]


4

คอมไพเลอร์ได้รับอนุญาตให้ทำการเรียงลำดับใหม่หรือเราสามารถไว้วางใจให้คอมไพเลอร์สังเกตเห็นความไม่สอดคล้องกันของผลลัพธ์และรักษาลำดับนิพจน์ตามที่เป็นอยู่ได้หรือไม่?

คอมไพลเลอร์สามารถจัดลำดับใหม่ได้ก็ต่อเมื่อให้ผลลัพธ์เหมือนกัน - ที่นี่ตามที่คุณสังเกตเห็นว่าไม่ได้


เป็นไปได้ที่จะเขียนเทมเพลตฟังก์ชันหากคุณต้องการซึ่งจะส่งเสริมอาร์กิวเมนต์ทั้งหมดstd::common_typeก่อนที่จะเพิ่มสิ่งนี้จะปลอดภัยและไม่ต้องพึ่งพาลำดับอาร์กิวเมนต์หรือการคัดเลือกด้วยตนเอง แต่ก็ค่อนข้างยุ่งยาก


ฉันรู้ว่าควรใช้การแคสต์อย่างชัดเจน แต่ฉันต้องการทราบพฤติกรรมของคอมไพเลอร์เมื่อการแคสต์ดังกล่าวถูกละเว้นโดยไม่ตั้งใจ
Tal

1
อย่างที่ฉันพูดไปโดยไม่ต้องแคสต์อย่างชัดเจน: การเพิ่มด้านซ้ายจะดำเนินการก่อนโดยไม่มีการส่งเสริมแบบอินทิกรัลดังนั้นจึงขึ้นอยู่กับการห่อ ผลของการเติมที่อาจจะห่อเป็นแล้วการเลื่อนตำแหน่งให้uint64_tสำหรับนอกจากขวาสุดคุ้มค่า
ไร้ประโยชน์

คำอธิบายของคุณเกี่ยวกับกฎ as-if นั้นผิดโดยสิ้นเชิง ตัวอย่างเช่นภาษา C ระบุการดำเนินการที่ควรเกิดขึ้นบนเครื่องนามธรรม กฎ "as-if" ช่วยให้สามารถทำสิ่งที่ต้องการได้อย่างแน่นอนตราบเท่าที่ไม่มีใครสามารถบอกความแตกต่างได้
gnasher729

ซึ่งหมายความว่าคอมไพลเลอร์สามารถทำอะไรก็ได้ที่ต้องการตราบเท่าที่ผลลัพธ์เหมือนกับที่กำหนดโดยการเชื่อมโยงด้านซ้ายและกฎการแปลงเลขคณิตที่แสดง
ไร้ประโยชน์

1

ขึ้นอยู่กับความกว้างบิตของunsigned/int.

2 ด้านล่างไม่เหมือนกัน (เมื่อunsigned <= 32บิต) u32_x + u32_yกลายเป็น 0

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

พวกมันเหมือนกัน (เมื่อunsigned >= 34บิต) การส่งเสริมจำนวนเต็มทำให้เกิดการ u32_x + u32_yเพิ่มขึ้นที่คณิตศาสตร์ 64 บิต คำสั่งซื้อไม่เกี่ยวข้อง

มันคือ UB (เมื่อunsigned == 33บิต) การส่งเสริมจำนวนเต็มทำให้เกิดการเพิ่มขึ้นในการคำนวณแบบ 33 บิตที่เซ็นชื่อและมีการลงนามมากเกินไปคือ UB

คอมไพเลอร์ได้รับอนุญาตให้ทำการเรียงลำดับใหม่ ... ?

(คณิตศาสตร์ 32 บิต): สั่งซื้อใหม่ใช่ แต่ผลลัพธ์เดียวกันจะต้องเกิดขึ้นดังนั้นไม่ใช่ว่าการสั่งซื้อ OP เสนอใหม่ ด้านล่างนี้เหมือนกัน

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... เราสามารถไว้วางใจให้พวกเขาสังเกตเห็นความไม่สอดคล้องกันของผลลัพธ์และรักษาลำดับการแสดงออกตามที่เป็นอยู่ได้หรือไม่?

เชื่อว่าใช่ แต่เป้าหมายการเข้ารหัสของ OP นั้นไม่ชัดเจน ควรu32_x + u32_yมีส่วนร่วม? หาก OP ต้องการการสนับสนุนนั้นรหัสควรเป็น

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

แต่ไม่

uint64_t u64_z = u32_x + u32_y + u64_a;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.