เหตุใด Clang จึงเพิ่มประสิทธิภาพออกไป x * 1.0 แต่ไม่ใช่ x + 0.0


125

เหตุใด Clang จึงเพิ่มประสิทธิภาพการวนซ้ำในโค้ดนี้

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

แต่ไม่วนซ้ำในรหัสนี้?

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(แท็กเป็นทั้ง C และ C ++ เพราะอยากทราบว่าคำตอบแต่ละข้อแตกต่างกันหรือไม่)


2
แฟล็กการเพิ่มประสิทธิภาพใดที่ใช้งานอยู่ในปัจจุบัน
Iwillnotexist Idonotexist

1
@IwillnotexistIdonotexist: ฉันเพิ่งใช้-O3ฉันไม่รู้วิธีตรวจสอบสิ่งที่เปิดใช้งาน
user541686

2
มันจะน่าสนใจที่จะเห็นว่าจะเกิดอะไรขึ้นถ้าคุณเพิ่ม -ffast-math ลงในบรรทัดคำสั่ง
plugwash

static double arr[N]ไม่ได้รับอนุญาตใน C; constตัวแปรไม่นับเป็นนิพจน์คงที่ในภาษานั้น
MM

1
[ใส่ความคิดเห็นที่น่ากลัวเกี่ยวกับวิธีที่ C ไม่ใช่ C ++ แม้ว่าคุณจะเรียกมันออกไปแล้วก็ตาม]
user253751

คำตอบ:


164

มาตรฐาน IEEE 754-2008 สำหรับเลขคณิต Floating-Point และมาตรฐาน ISO / IEC 10967 Language Independent Arithmetic (LIA) ตอนที่ 1 เป็นคำตอบว่าเหตุใดจึงเป็นเช่นนั้น

IEEE 754 § 6.3 บิตเครื่องหมาย

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

เมื่อไม่มีอินพุตหรือผลลัพธ์เป็น NaN เครื่องหมายของผลิตภัณฑ์หรือผลหารจะเป็นเอกสิทธิ์เฉพาะหรือของสัญญาณของตัวถูกดำเนินการ เครื่องหมายของผลรวมหรือผลต่าง x - y ถือเป็นผลรวม x + (−y) แตกต่างจากเครื่องหมายบวกส่วนใหญ่ และเครื่องหมายของผลลัพธ์ของการแปลงการดำเนินการเชิงปริมาณการดำเนินการ roundTo-Integral และ roundToIntegralExact (ดู 5.3.1) เป็นเครื่องหมายของตัวถูกดำเนินการตัวแรกหรือตัวเดียว กฎเหล่านี้จะใช้แม้ว่าตัวถูกดำเนินการหรือผลลัพธ์จะเป็นศูนย์หรือไม่มีที่สิ้นสุด

เมื่อผลรวมของสองตัวถูกดำเนินการที่มีเครื่องหมายตรงข้าม (หรือผลต่างของตัวถูกดำเนินการสองตัวที่มีสัญลักษณ์เหมือนกัน) เป็นศูนย์พอดีเครื่องหมายของผลรวมนั้น (หรือผลต่าง) จะเป็น +0 ในแอตทริบิวต์ทิศทางการปัดเศษทั้งหมดยกเว้น roundTowardNegative ภายใต้แอตทริบิวต์นั้นเครื่องหมายของผลรวมศูนย์ที่แน่นอน (หรือผลต่าง) จะต้องเป็น −0 อย่างไรก็ตาม x + x = x - (−x) ยังคงมีเครื่องหมายเดียวกับ x แม้ว่า x จะเป็นศูนย์ก็ตาม

กรณีของการเพิ่ม

ภายใต้โหมดการปัดเศษเริ่มต้น (Round-to-Nearest, Ties-to-Even)เราจะเห็นว่าx+0.0สร้างxขึ้นยกเว้นเมื่อxเป็น-0.0: ในกรณีนั้นเรามีผลรวมของสองตัวถูกดำเนินการที่มีเครื่องหมายตรงข้ามซึ่งผลรวมเป็นศูนย์และ§6.3ย่อหน้า 3 +0.0กฎนอกจากนี้ผลิต

เนื่องจาก+0.0ไม่ได้เป็นบิตเหมือนกันกับต้นฉบับ-0.0และนั่น-0.0เป็นค่าที่ถูกต้องซึ่งอาจเกิดขึ้นเป็นอินพุตคอมไพเลอร์จึงจำเป็นต้องใส่รหัสที่จะเปลี่ยนค่าศูนย์ลบที่อาจเกิดขึ้นเป็น+0.0เป็น

สรุป: ภายใต้โหมดการปัดเศษเริ่มต้นในx+0.0ถ้าx

  • ไม่ได้ -0.0แล้วxตัวเองเป็นมูลค่าการส่งออกได้รับการยอมรับ
  • คือ -0.0ค่าเอาต์พุตจะต้องเป็น +0.0ซึ่งไม่เหมือนกับบิตคอย-0.0น์

กรณีของการคูณ

ภายใต้โหมดเริ่มต้นการปัดเศษx*1.0ไม่มีปัญหาดังกล่าวเกิดขึ้นกับ ถ้าx:

  • เป็น (ย่อย) จำนวนปกติ x*1.0 == xเสมอ
  • เป็น+/- infinityแล้วผลที่ได้คือ+/- infinityการเข้าสู่ระบบเดียวกัน
  • เป็นNaNแล้วตาม

    IEEE 754 § 6.2.3 NaN การขยายพันธุ์

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

    ซึ่งหมายความว่าตัวแทนและ mantissa (แม้ว่าจะไม่ได้เข้าสู่ระบบ) ของNaN*1.0จะแนะนำNaNที่จะไม่เปลี่ยนแปลงจากการป้อนข้อมูล เป็นเครื่องหมายที่ไม่ระบุสอดคล้องกับ§6.3p1ข้างต้น NaNแต่การดำเนินการอาจจะระบุว่ามันจะเหมือนกันกับแหล่งที่มา

  • คือ+/- 0.0แล้วผลลัพธ์จะเป็น0ด้วยบิตเครื่องหมาย XORed กับบิตเครื่องหมายของ1.0ตามข้อตกลงกับ§6.3p2 เนื่องจากบิตของเครื่องหมาย1.0คือ0ค่าเอาต์พุตจะไม่เปลี่ยนแปลงจากอินพุต ดังนั้นx*1.0 == xแม้ว่าเมื่อใดที่xเป็นศูนย์ (ลบ)

กรณีของการลบ

ภายใต้โหมดการปัดเศษเริ่มต้น , การลบx-0.0ยังเป็น no-op x + (-0.0)เพราะมันจะเทียบเท่ากับ ถ้าxเป็น

  • คือNaNแล้ว§6.3p1และ§6.2.3จะใช้ในลักษณะเดียวกับการบวกและการคูณ
  • เป็น+/- infinityแล้วผลที่ได้คือ+/- infinityการเข้าสู่ระบบเดียวกัน
  • เป็น (ย่อย) จำนวนปกติx-0.0 == xเสมอ
  • คือ-0.0แล้วโดย§6.3p2เรามี " [... ] เครื่องหมายของผลรวมหรือผลต่าง x - y ถือเป็นผลรวม x + (−y) แตกต่างจากเครื่องหมายบวกส่วนใหญ่ " กองกำลังนี้เราจะกำหนด-0.0เป็นผลมาจาก(-0.0) + (-0.0)เพราะ-0.0ความแตกต่างในการเข้าสู่ระบบจากใครของ addends ในขณะที่+0.0มีความแตกต่างในการเข้าสู่ระบบจากสองส่วนที่เพิ่มซึ่งเป็นการละเมิดข้อนี้
  • คือ+0.0จากนั้นสิ่งนี้จะลดลงเป็นกรณีการเพิ่มที่(+0.0) + (-0.0)พิจารณาข้างต้นในกรณีของการเพิ่ม+0.0ซึ่งโดย§6.3p3ถูกปกครองที่จะให้

เนื่องจากสำหรับทุกกรณีค่าอินพุตถูกต้องตามกฎหมายเป็นเอาต์พุตจึงอนุญาตให้พิจารณาx-0.0no-op และx == x-0.0tautology ได้

การเพิ่มประสิทธิภาพการเปลี่ยนแปลงมูลค่า

มาตรฐาน IEEE 754-2008 มีคำพูดที่น่าสนใจดังต่อไปนี้:

IEEE 754 § 10.4 ความหมายตามตัวอักษรและการปรับเปลี่ยนค่าให้เหมาะสม

[ ... ]

การแปลงค่าที่เปลี่ยนแปลงต่อไปนี้และอื่น ๆ รักษาความหมายตามตัวอักษรของซอร์สโค้ด:

  • การใช้คุณสมบัติเอกลักษณ์ 0 + x เมื่อ x ไม่ใช่ศูนย์และไม่ใช่ NaN การส่งสัญญาณและผลลัพธ์จะมีเลขชี้กำลังเหมือนกับ x
  • การใช้คุณสมบัติเอกลักษณ์ 1 × x เมื่อ x ไม่ใช่ NaN การส่งสัญญาณและผลลัพธ์จะมีเลขชี้กำลังเหมือนกับ x
  • การเปลี่ยน payload หรือ sign bit ของ NaN ที่เงียบ
  • [ ... ]

เนื่องจาก NaN ทั้งหมดและ infinities ทั้งหมดมีเลขชี้กำลังเหมือนกันและผลลัพธ์ที่ถูกปัดเศษอย่างถูกต้องของx+0.0และx*1.0สำหรับ จำกัดxมีขนาดเท่าxกันทุกประการเลขชี้กำลังจึงเหมือนกัน

sNaNs

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

อย่างไรก็ตามตามที่ user2357112 ชี้ให้เห็นในความคิดเห็น C11 Standard จะทิ้งพฤติกรรมการส่งสัญญาณ NaNs ( sNaN) โดยไม่ได้กำหนดไว้อย่างชัดเจนดังนั้นคอมไพเลอร์จึงได้รับอนุญาตให้ถือว่าสิ่งเหล่านี้ไม่เกิดขึ้นและดังนั้นจึงไม่มีข้อยกเว้นที่พวกเขาเพิ่มขึ้นเช่นกัน มาตรฐาน C ++ 11 จะละเว้นที่อธิบายถึงพฤติกรรมในการส่งสัญญาณ NaN และทำให้ไม่ได้กำหนดไว้

โหมดการปัดเศษ

ในโหมดการปัดเศษอื่นการเพิ่มประสิทธิภาพที่อนุญาตอาจเปลี่ยนแปลงได้ ตัวอย่างเช่นภายใต้โหมดRound-to-Negative-Infinityการเพิ่มประสิทธิภาพx+0.0 -> xจะได้รับอนุญาต แต่x-0.0 -> xจะถูกห้าม

เพื่อป้องกันไม่ให้ GCC สมมติว่าเป็นโหมดและพฤติกรรมการปัดเศษเริ่มต้น-frounding-mathคุณสามารถส่งค่าสถานะทดลองไปยัง GCC ได้

ข้อสรุป

เสียงดังกราวและGCCแม้ที่-O3ยังคงมาตรฐาน IEEE-754 ตามมาตรฐาน ซึ่งหมายความว่าจะต้องปฏิบัติตามกฎข้างต้นของมาตรฐาน IEEE-754 x+0.0คือไม่บิตเหมือนกันไปxทั้งหมดxภายใต้กฎระเบียบเหล่านั้น แต่x*1.0 อาจจะเลือกที่จะให้ : คือเมื่อเรา

  1. ปฏิบัติตามคำแนะนำเพื่อส่งผ่าน payload ที่ไม่มีการเปลี่ยนแปลงxเมื่อเป็น NaN
  2. * 1.0ปล่อยบิตเครื่องหมายของน่านส่งผลให้เกิดการเปลี่ยนแปลงโดย
  3. เชื่อฟังคำสั่งให้แฮคเกอร์บิตเครื่องหมายระหว่างความฉลาดทาง / สินค้าเมื่อxเป็นไม่น่าน

ในการเปิดใช้งานการเพิ่มประสิทธิภาพ IEEE-754 ที่ไม่ปลอดภัยต้องส่ง(x+0.0) -> xแฟ-ffast-mathล็กไปยัง Clang หรือ GCC


2
Caveat: ถ้าเป็นสัญญาณ NaN ล่ะ? (อันที่จริงฉันคิดว่านั่นอาจเป็นสาเหตุ แต่ฉันไม่รู้จริงๆฉันเลยถาม)
user541686

6
@ Mehrdad: ภาคผนวก F ส่วน (เป็นทางเลือก) ของมาตรฐาน C ที่ระบุการยึดมั่น C กับ IEEE 754 อย่างชัดเจนไม่ครอบคลุมถึง NaN ของการส่งสัญญาณ (C11 F.2.1. บรรทัดแรก: "ข้อกำหนดนี้ไม่ได้กำหนดลักษณะการทำงานของการส่งสัญญาณ NaNs") การดำเนินการที่ประกาศว่าเป็นไปตามภาคผนวก F ยังคงมีอิสระที่จะทำสิ่งที่พวกเขาต้องการด้วยการส่งสัญญาณ NaN มาตรฐาน C ++ มีการจัดการ IEEE 754 ของตัวเอง แต่ไม่ว่าจะเป็นอะไร (ฉันไม่คุ้นเคย) ฉันสงสัยว่ามันระบุพฤติกรรมการส่งสัญญาณของ NaN ด้วย
user2357112 รองรับ Monica

2
@Mehrdad: sNaN เรียกใช้พฤติกรรมที่ไม่ได้กำหนดตามมาตรฐาน (แต่แพลตฟอร์มอาจกำหนดไว้อย่างดี) ดังนั้นจึงอนุญาตให้คอมไพเลอร์บีบที่นี่ได้
Joshua

1
@ user2357112: ความเป็นไปได้ของการดักจับข้อผิดพลาดเป็นผลข้างเคียงสำหรับการคำนวณที่ไม่ได้ใช้โดยทั่วไปจะรบกวนการเพิ่มประสิทธิภาพจำนวนมาก หากผลของการคำนวณถูกละเว้นในบางครั้งคอมไพเลอร์อาจเลื่อนการคำนวณออกไปอย่างมีประโยชน์จนกว่าจะสามารถรู้ได้ว่าผลลัพธ์จะถูกใช้หรือไม่ แต่ถ้าการคำนวณจะให้สัญญาณที่สำคัญอาจเป็นผลเสียได้
supercat

2
ดูสิคำถามที่ใช้ได้กับทั้ง C และ C ++ อย่างถูกต้องซึ่งได้รับคำตอบอย่างถูกต้องสำหรับทั้งสองภาษาโดยอ้างอิงมาตรฐานเดียว สิ่งนี้จะทำให้ผู้คนไม่ค่อยบ่นเกี่ยวกับคำถามที่ติดแท็กทั้ง C และ C ++ แม้ว่าคำถามจะเกี่ยวข้องกับภาษาทั่วไปหรือไม่ น่าเศร้าที่ฉันคิดว่าไม่
Kyle Strand

35

x += 0.0ไม่ได้เป็น NOOP ถ้าเป็นx -0.0เครื่องมือเพิ่มประสิทธิภาพสามารถตัดลูปทั้งหมดออกได้เนื่องจากไม่ได้ใช้ผลลัพธ์ โดยทั่วไปยากที่จะบอกได้ว่าเหตุใดเครื่องมือเพิ่มประสิทธิภาพจึงตัดสินใจทำเช่นนั้น


2
ฉันโพสต์สิ่งนี้หลังจากที่ฉันเพิ่งอ่านว่าเหตุใดจึงx += 0.0ไม่ใช่ no-op แต่ฉันคิดว่านั่นอาจไม่ใช่เหตุผลเพราะควรปรับลูปทั้งหมดให้เหมาะสมไม่ว่าจะด้วยวิธีใดก็ตาม ฉันสามารถซื้อได้ แต่ก็ไม่น่าเชื่ออย่างที่หวังไว้ทั้งหมด ...
user541686

เมื่อพิจารณาถึงแนวโน้มที่ภาษาเชิงวัตถุในการสร้างผลข้างเคียงฉันคิดว่าคงเป็นเรื่องยากที่จะแน่ใจว่าเครื่องมือเพิ่มประสิทธิภาพไม่ได้เปลี่ยนพฤติกรรมจริง
Robert Harvey

อาจเป็นเหตุผลเนื่องจากlong longการเพิ่มประสิทธิภาพมีผล (ทำกับ gcc ซึ่งทำงานเหมือนกันอย่างน้อยสองเท่า )
e2-e4

2
@ ringø: long longเป็นประเภทอินทิกรัลไม่ใช่ประเภท IEEE754
MSalters

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