ทำไม GCC จึงไม่ปรับ a * a * a * a * a ถึง (a * a * a) * (a * a * a)?


2120

ฉันกำลังทำการเพิ่มประสิทธิภาพเชิงตัวเลขในแอปพลิเคชันทางวิทยาศาสตร์ สิ่งหนึ่งที่ฉันสังเกตเห็นคือ GCC จะเพิ่มประสิทธิภาพการโทรpow(a,2)โดยรวบรวมมันเข้าไปa*aแต่การโทรpow(a,6)นั้นไม่ได้รับการปรับปรุงและจะเรียกฟังก์ชั่นห้องสมุดpowซึ่งทำให้ประสิทธิภาพช้าลงอย่างมาก (ตรงกันข้ามIntel C ++ Compiler ที่สามารถเรียกทำงานiccได้จะกำจัดการเรียกใช้ไลบรารีpow(a,6))

สิ่งที่ฉันอยากรู้คือเมื่อฉันแทนที่pow(a,6)ด้วยการa*a*a*a*a*aใช้ GCC 4.5.1 และตัวเลือก " -O3 -lm -funroll-loops -msse4" จะใช้ 5 mulsdคำสั่ง:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

ในขณะที่ถ้าฉันเขียน(a*a*a)*(a*a*a)มันจะผลิต

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

ซึ่งลดจำนวนคำสั่งการคูณเป็น 3 iccมีพฤติกรรมที่คล้ายกัน

ทำไมคอมไพเลอร์ไม่รู้จักเคล็ดลับการเพิ่มประสิทธิภาพนี้


13
"การรับรู้ pow (a, 6)" หมายถึงอะไร
Varun Madiath

659
อืม ... คุณรู้ว่าและ (กก) * (เป็น A * ก) ไม่ได้เช่นเดียวกับตัวเลขทศนิยม, คุณไม่? คุณจะต้องใช้ -funsafe-math หรือ -ffast-math หรืออะไรทำนองนั้น
Damon

106
ฉันขอแนะนำให้คุณอ่าน "สิ่งที่นักวิทยาศาสตร์คอมพิวเตอร์ทุกคนควรรู้เกี่ยวกับเลขทศนิยม" โดย David Goldberg: download.oracle.com/docs/cd/E19957-01/806-3568/…หลังจากนั้นคุณจะมีความเข้าใจที่สมบูรณ์ยิ่งขึ้นเกี่ยวกับ หลุมน้ำมันดินที่คุณเพิ่งเดินเข้าไป!
Phil Armstrong

189
คำถามที่เหมาะสมอย่างสมบูรณ์ 20 ปีที่แล้วฉันถามคำถามทั่วไปเดียวกันและโดยการบดขวดคอเดียวนั้นลดเวลาในการดำเนินการของการจำลอง Monte Carlo จาก 21 ชั่วโมงเป็น 7 ชั่วโมง โค้ดในลูปภายในถูกประมวลผล 13 ล้านล้านครั้งในกระบวนการ แต่มันมีการจำลองเข้าไปในหน้าต่างข้ามคืน (ดูคำตอบด้านล่าง)

23
อาจโยน(a*a)*(a*a)*(a*a)ลงในส่วนผสมเช่นกัน จำนวนทวีคูณเท่ากัน แต่อาจแม่นยำกว่า
Rok Kralj

คำตอบ:


2738

เพราะFloating Point คณิตศาสตร์ไม่ได้เชื่อมโยง วิธีที่คุณจัดกลุ่มตัวถูกดำเนินการในการคูณคะแนนแบบลอยมีผลต่อความแม่นยำเชิงตัวเลขของคำตอบ

ดังนั้นคอมไพเลอร์ส่วนใหญ่ค่อนข้างอนุรักษ์นิยมมากเกี่ยวกับการคำนวณการคำนวณจุดลอยตัวใหม่จนกว่าพวกเขาจะมั่นใจได้ว่าคำตอบจะยังคงเหมือนเดิมหรือถ้าคุณบอกพวกเขาว่าคุณไม่สนใจความถูกต้องเชิงตัวเลข ตัวอย่างเช่น: ตัวเลือกของ GCC GCC ซึ่งจะช่วยให้การดำเนินงาน reassociate จุดลอยหรือแม้กระทั่งตัวเลือกซึ่งจะช่วยให้เกิดความสมดุลแม้ก้าวร้าวมากขึ้นของความถูกต้องกับความเร็ว-fassociative-math-ffast-math


10
ใช่. ด้วย -ffast-math มันกำลังทำการปรับให้เหมาะสมเช่นนั้น ความคิดที่ดี! แต่เนื่องจากรหัสของเราเกี่ยวข้องกับความแม่นยำมากกว่าความเร็วจึงอาจไม่ควรผ่าน
xis

19
IIRC C99 อนุญาตให้คอมไพเลอร์ทำการเพิ่มประสิทธิภาพ FP ที่ "ไม่ปลอดภัย" แต่ GCC (ในสิ่งอื่นที่ไม่ใช่ x87) พยายามอย่างสมเหตุสมผลในการติดตาม IEEE 754 - ไม่ใช่ขอบเขตข้อผิดพลาด "; มีเพียงหนึ่งคำตอบที่ถูก
tc

14
รายละเอียดการใช้งานของpowไม่ได้อยู่ที่นี่หรือที่นั่น powคำตอบนี้ไม่อ้างอิงไม่ได้
สตีเฟ่นแคนนอน

14
@nedR: ค่าเริ่มต้นของ ICC เพื่ออนุญาตการเชื่อมโยงใหม่ หากคุณต้องการได้รับพฤติกรรมที่สอดคล้องกับมาตรฐานคุณจะต้องตั้งค่า-fp-model preciseด้วย ICC clangและgccเริ่มต้นที่ reassociation สอดคล้องกันอย่างเข้มงวด wrt
Stephen Canon

49
@xis มันไม่ใช่ความจริงที่-fassociative-mathจะผิดพลาด เป็นเพียงแค่นั้นa*a*a*a*a*aและ(a*a*a)*(a*a*a)แตกต่าง มันไม่เกี่ยวกับความแม่นยำ มันเกี่ยวกับความสอดคล้องมาตรฐานและผลลัพธ์ที่ทำซ้ำได้อย่างเคร่งครัดเช่นผลลัพธ์เดียวกันกับคอมไพเลอร์ใด ๆ ตัวเลขทศนิยมไม่ชัดเจน มันไม่เหมาะสมที่จะรวบรวม-fassociative-mathบ่อยครั้ง
พอลเดรเปอร์

652

Lambdageekชี้ให้เห็นอย่างถูกต้องว่าเนื่องจากความสัมพันธ์ไม่ได้เก็บไว้สำหรับเลขทศนิยม, "การเพิ่มประสิทธิภาพ" ของa*a*a*a*a*aถึง(a*a*a)*(a*a*a)อาจเปลี่ยนค่า นี่คือเหตุผลที่มันไม่ได้รับอนุญาตจาก C99 (ยกเว้นกรณีที่ได้รับอนุญาตจากผู้ใช้ผ่านธงคอมไพเลอร์หรือ pragma) โดยทั่วไปแล้วสมมติฐานคือโปรแกรมเมอร์เขียนสิ่งที่เธอทำด้วยเหตุผลและคอมไพเลอร์ควรเคารพสิ่งนั้น ถ้าคุณต้องการ(a*a*a)*(a*a*a)เขียนว่า

นั่นอาจเป็นความเจ็บปวดที่จะเขียนแม้ว่า; ทำไมคอมไพเลอร์ไม่สามารถทำ [สิ่งที่คุณคิดว่าเป็น] สิ่งที่ถูกต้องเมื่อคุณใช้pow(a,6)? เพราะมันจะเป็นสิ่งที่ผิดที่ต้องทำ บนแพลตฟอร์มที่มีห้องสมุดคณิตศาสตร์ที่ดีpow(a,6)เป็นอย่างถูกต้องมากขึ้นกว่าหรือa*a*a*a*a*a (a*a*a)*(a*a*a)เพื่อให้ข้อมูลบางอย่างฉันทำการทดลองขนาดเล็กบน Mac Pro ของฉันวัดข้อผิดพลาดที่เลวร้ายที่สุดในการประเมิน ^ 6 สำหรับตัวเลขลอยตัวที่มีความแม่นยำเดี่ยวทั้งหมดระหว่าง [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

ใช้powแทนของต้นไม้คูณช่วยลดข้อผิดพลาดผูกพันตามปัจจัย 4 คอมไพเลอร์ไม่ควร (และโดยทั่วไปจะไม่ทำ) "เพิ่มประสิทธิภาพ" ที่เพิ่มข้อผิดพลาดเว้นแต่ได้รับอนุญาตจากผู้ใช้ (เช่นผ่าน-ffast-math)

โปรดทราบว่า GCC __builtin_powi(x,n)เป็นทางเลือกpow( )ซึ่งควรสร้างแผนผังการคูณแบบอินไลน์ ใช้สิ่งนั้นหากคุณต้องการแลกเปลี่ยนความแม่นยำเพื่อประสิทธิภาพ แต่ไม่ต้องการเปิดใช้งานคณิตศาสตร์อย่างรวดเร็ว


29
โปรดสังเกตว่า Visual C ++ ให้ pow () ที่ได้รับการปรับปรุงแล้ว ด้วยการโทร_set_SSE2_enable(<flag>)ด้วยflag=1จะใช้ SSE2 หากเป็นไปได้ สิ่งนี้ช่วยลดความแม่นยำลงเล็กน้อย แต่ปรับปรุงความเร็ว (ในบางกรณี) MSDN: _set_SSE2_enable ()และpow ()
TkTech

18
@TkTech: ความแม่นยำที่ลดลงเกิดจากการใช้งานของ Microsoft ไม่ใช่ขนาดของรีจิสเตอร์ที่ใช้ เป็นไปได้ที่จะนำเสนอการปัดเศษที่ถูกต้อง powโดยใช้การลงทะเบียนแบบ 32 บิตเท่านั้นหากผู้เขียนไลบรารีมีแรงจูงใจมาก มีpowการใช้งานที่ใช้SSE ที่มีความแม่นยำมากกว่าการใช้งานที่ใช้ x87 ส่วนใหญ่และยังมีการใช้งานที่แลกเปลี่ยนความแม่นยำสำหรับความเร็ว
สตีเฟ่น Canon

9
@TkTech: แน่นอนว่าฉันต้องการทำให้ชัดเจนว่าการลดความแม่นยำนั้นเกิดจากตัวเลือกของนักเขียนห้องสมุดไม่ใช่ที่แท้จริงของการใช้ SSE
Stephen Canon

7
ฉันสนใจที่จะรู้ว่าสิ่งที่คุณใช้เป็น "มาตรฐานทองคำ" ที่นี่สำหรับการคำนวณข้อผิดพลาดสัมพัทธ์ - ปกติฉันคาดว่ามันจะเป็นa*a*a*a*a*aเช่นนั้น แต่นั่นไม่ใช่กรณี! :)
j_random_hacker

8
@j_random_hacker: ตั้งแต่ฉันเปรียบเทียบผลลัพธ์ความแม่นยำเดี่ยวความแม่นยำสองเท่าสำหรับมาตรฐานทองคำ - ข้อผิดพลาดจากa a a a ที่คำนวณเป็นสองเท่านั้นมีขนาดเล็กกว่าข้อผิดพลาดของการคำนวณความแม่นยำเดียวใด ๆ
Stephen Canon

168

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

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

ผลลัพธ์นี้ 1.000000e-05 0.000000e+00


10
สิ่งนี้ไม่เหมือนกันทุกประการ เปลี่ยนลำดับการคูณ / ดิวิชั่น (ยกเว้นการหารด้วย 0) จะปลอดภัยกว่าลำดับการเปลี่ยน / การลบ ในความเห็นที่ต่ำต้อยของฉันคอมไพเลอร์ควรพยายามเชื่อมโยง mults.divs เพราะการทำเช่นนั้นจะช่วยลดจำนวนการปฏิบัติงานทั้งหมดและนอกเหนือจากการเพิ่มประสิทธิภาพที่ได้นั้นยังเป็นการเพิ่มความแม่นยำอีกด้วย
CoffeDeveloper

4
@DarioOO: มันไม่ปลอดภัยกว่า การคูณและหารนั้นเหมือนกับการบวกและการลบของเลขชี้กำลังและการเปลี่ยนคำสั่งสามารถทำให้ขมับเกินขอบเขตของเลขชี้กำลังที่เป็นไปได้ (ไม่เหมือนกันเพราะเลขชี้กำลังไม่สูญเสียความแม่นยำ ... แต่การแสดงยังค่อนข้าง จำกัด และการเรียงลำดับใหม่สามารถนำไปสู่ค่าที่ไม่สามารถคาดเดาได้)
Ben Voigt

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

8
@DarioOO: ความเสี่ยงแตกต่างจาก mul / div: การเรียงลำดับใหม่อาจทำให้การเปลี่ยนแปลงเล็กน้อยในผลลัพธ์สุดท้ายหรือการโอเวอร์โฟลว์แบบ exponent ในบางจุด (ซึ่งไม่เคยมีมาก่อน) และผลลัพธ์นั้นแตกต่างกันอย่างมาก 0)
Peter Cordes

@GameDeveloper การเพิ่มความแม่นยำในรูปแบบที่ไม่สามารถคาดการณ์ได้นั้นเป็นปัญหาอย่างมหาศาล
curiousguy

80

Fortran (ออกแบบมาสำหรับการคำนวณทางวิทยาศาสตร์) มีตัวดำเนินการจ่ายไฟในตัวและเท่าที่ฉันรู้คอมไพเลอร์ Fortran โดยทั่วไปจะปรับการเพิ่มประสิทธิภาพให้เป็นจำนวนเต็มในแบบเดียวกับที่คุณอธิบาย C / C ++ pow()น่าเสียดายที่ไม่ได้มีผู้ประกอบการพลังงานเพียงฟังก์ชั่นห้องสมุด สิ่งนี้ไม่ได้ป้องกันคอมไพเลอร์สมาร์ทจากการปฏิบัติpowเป็นพิเศษและคำนวณมันในวิธีที่เร็วขึ้นสำหรับกรณีพิเศษ แต่ดูเหมือนว่าพวกเขาจะทำมันน้อยกว่าปกติ ...

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

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

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

power<6>(a)แล้วก็ใช้มันเป็น

สิ่งนี้ทำให้ง่ายต่อการพิมพ์พลัง (ไม่จำเป็นต้องสะกดคำ 6 as ด้วย parens) และช่วยให้คุณมีการเพิ่มประสิทธิภาพแบบนี้โดยไม่ต้องใช้-ffast-mathในกรณีที่คุณมีสิ่งที่ต้องอาศัยความแม่นยำเช่นการสรุปผลรวม (ตัวอย่างที่จำเป็น .

คุณอาจลืมได้ว่านี่คือ C ++ และใช้ในโปรแกรม C (ถ้าคอมไพล์ด้วยคอมไพเลอร์ C ++)

หวังว่ามันจะมีประโยชน์

แก้ไข:

นี่คือสิ่งที่ฉันได้รับจากคอมไพเลอร์ของฉัน:

สำหรับa*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

สำหรับ(a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

สำหรับpower<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

36
การค้นหาต้นกำลังที่เหมาะสมอาจเป็นเรื่องยาก แต่เนื่องจากมันน่าสนใจสำหรับพลังขนาดเล็กเท่านั้นคำตอบที่ชัดเจนคือการคำนวณล่วงหน้าหนึ่งครั้ง (Knuth ให้ตารางสูงถึง 100) และใช้ตาราง hardcoded (นั่นคือสิ่งที่ gcc ทำภายในสำหรับ powi) .
Marc Glisse

7
สำหรับโปรเซสเซอร์รุ่นใหม่ความเร็วนั้นถูก จำกัด ด้วยความหน่วงแฝง ตัวอย่างเช่นผลลัพธ์ของการคูณอาจพร้อมใช้งานหลังจากห้ารอบ ในสถานการณ์นั้นการหาวิธีที่เร็วที่สุดในการสร้างพลังบางอย่างนั้นอาจจะยุ่งยากกว่า
gnasher729

3
คุณสามารถลองค้นหาต้นไม้พลังงานที่ให้ขอบเขตบนที่ต่ำที่สุดสำหรับข้อผิดพลาดในการปัดเศษแบบสัมพัทธ์หรือข้อผิดพลาดในการปัดเศษโดยเฉลี่ยที่ต่ำที่สุด
gnasher729

1
Boost ยังรองรับสิ่งนี้เช่น boost :: math :: pow <6> (n); ฉันคิดว่ามันจะพยายามลดจำนวนการคูณด้วยการแยกปัจจัยทั่วไป
gast128

โปรดทราบว่าอันสุดท้ายเทียบเท่ากับ (a ** 2) ** 3
minmaxavg

62

GCC จะปรับa*a*a*a*a*aให้เหมาะสมจริง ๆ(a*a*a)*(a*a*a)เมื่อ a เป็นจำนวนเต็ม ฉันลองด้วยคำสั่งนี้:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

มีธง gcc มากมาย แต่ไม่มีอะไรแฟนซี พวกเขาหมายถึง: อ่านจาก stdin; ใช้ระดับการเพิ่มประสิทธิภาพ O2; รายการภาษาแอสเซมบลีเอาท์พุทแทนไบนารี รายการควรใช้ไวยากรณ์ภาษาแอสเซมบลีของ Intel อินพุตเป็นภาษา C (โดยปกติภาษาจะอนุมานจากนามสกุลไฟล์อินพุต แต่ไม่มีนามสกุลไฟล์เมื่ออ่านจาก stdin); และเขียนถึง stdout

นี่คือส่วนสำคัญของผลลัพธ์ ฉันได้ใส่คำอธิบายประกอบไว้ด้วยความคิดเห็นที่บ่งบอกว่าเกิดอะไรขึ้นในภาษาแอสเซมบลี:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

ฉันใช้ระบบ GCC บน Linux Mint 16 Petra ซึ่งเป็นอนุพันธ์ของ Ubuntu นี่คือรุ่น gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

ดังที่ผู้โพสต์คนอื่น ๆ ได้ระบุไว้ตัวเลือกนี้ไม่สามารถทำได้ในจุดลอยตัวเนื่องจากเลขคณิตจุดลอยตัวนั้นไม่ได้เชื่อมโยงกัน


12
สิ่งนี้ถูกกฎหมายสำหรับการคูณจำนวนเต็มเพราะการเติมเต็มสองส่วนเกินเป็นพฤติกรรมที่ไม่ได้กำหนด หากจะมีการล้นก็จะเกิดขึ้นที่ไหนสักแห่งโดยไม่คำนึงถึงการดำเนินการสั่งใหม่ ดังนั้นนิพจน์ที่ไม่มีโอเวอร์โฟลว์จะประเมินค่าเหมือนกันนิพจน์ที่โอเวอร์โฟลนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นจึงเป็นเรื่องปกติที่คอมไพเลอร์จะเปลี่ยนจุดที่เกิดโอเวอร์โฟลว์ gcc ทำเช่นนี้ด้วยunsigned intเช่นกัน
Peter Cordes

51

เพราะตัวเลขทศนิยม 32 บิต - เช่น 1.024 - ไม่ใช่ 1.024 ในคอมพิวเตอร์ 1.024 คือช่วงเวลา: จาก (1.024-e) ถึง (1.024 + e) ​​โดยที่ "e" หมายถึงข้อผิดพลาด บางคนไม่เข้าใจสิ่งนี้และเชื่อว่า * ใน * a หมายถึงการคูณจำนวนที่มีความแม่นยำโดยไม่มีข้อผิดพลาดใด ๆ สาเหตุที่บางคนล้มเหลวในการตระหนักถึงสิ่งนี้อาจเป็นเพราะการคำนวณทางคณิตศาสตร์ที่พวกเขาใช้ในโรงเรียนประถมศึกษา: การทำงานกับตัวเลขในอุดมคติโดยไม่มีข้อผิดพลาดติดอยู่และเชื่อว่ามันเป็นเรื่องปกติ พวกเขาไม่เห็น "e" โดยนัยใน "float a = 1.2", "a * a * a" และรหัส C ที่คล้ายกัน

หากโปรแกรมเมอร์ส่วนใหญ่รู้จัก (และสามารถใช้งานได้) ความคิดที่ว่าการแสดงออกของ C * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * a * a * a * a "พูด" t = (a * a); t * t * t "ซึ่งต้องการการคูณที่น้อยกว่า แต่น่าเสียดายที่คอมไพเลอร์ GCC ไม่ทราบว่าโปรแกรมเมอร์ที่เขียนโค้ดคิดว่า "a" เป็นตัวเลขที่มีหรือไม่มีข้อผิดพลาด ดังนั้น GCC จะทำในสิ่งที่ซอร์สโค้ดมีลักษณะเท่านั้น - เพราะนั่นคือสิ่งที่ GCC เห็นด้วย "ตาเปล่า"

... เมื่อคุณรู้ว่าคุณเป็นโปรแกรมเมอร์ประเภทใดคุณสามารถใช้สวิตช์ "-ffast-math" เพื่อบอก GCC ว่า "เฮ้ GCC ฉันรู้ว่าฉันกำลังทำอะไรอยู่!" วิธีนี้จะช่วยให้ GCC แปลง * a * a * a * a เป็นข้อความอื่นได้ - มันดูแตกต่างจาก * a * a * a * a * a - a แต่ยังคงคำนวณตัวเลขภายในช่วงข้อผิดพลาดของ A * A * A * A * ที่ * ไม่เป็นไรเพราะคุณรู้อยู่แล้วว่าคุณกำลังทำงานกับช่วงเวลาไม่ใช่ตัวเลขในอุดมคติ


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

3
@DonalFellows: มาตรฐาน IEEE กำหนดว่าการคำนวณจุดลอยตัวจะให้ผลลัพธ์ที่ตรงกับผลลัพธ์มากที่สุดหากตัวถูกดำเนินการต้นทางเป็นค่าที่แน่นอน แต่ไม่ได้หมายความว่าพวกเขาเป็นตัวแทนของค่าที่แน่นอน ในหลายกรณีมีประโยชน์มากกว่าที่จะคำนึงถึง 0.1f ว่าเป็น (1,677,722 +/- 0.5) / 16,777,216 ซึ่งควรแสดงด้วยจำนวนตัวเลขทศนิยมที่บ่งบอกถึงความไม่แน่นอนนั้นมากกว่าที่จะถือว่าเป็นปริมาณที่แน่นอน (1,677,722 +/- 0.5) / 16,777,216 (ซึ่งควรแสดงเป็นตัวเลขทศนิยม 24 หลัก)
supercat

23
@supercat: IEEE-754 ค่อนข้างชัดเจนในจุดที่ข้อมูลเลขทศนิยมทำหน้าที่แทนค่าที่แน่นอน ข้อ 3.2 - 3.4 เป็นส่วนที่เกี่ยวข้อง แน่นอนคุณสามารถเลือกที่จะตีความพวกเขาเป็นอย่างอื่นเช่นเดียวกับที่คุณสามารถเลือกที่จะตีความint x = 3ตามความหมายที่x3 +/- 0.5
สตีเฟ่น Canon

7
@supercat: ฉันเห็นด้วยอย่างสิ้นเชิง แต่นั่นไม่ได้หมายความว่าDistanceไม่เท่ากับค่าตัวเลขอย่างแน่นอน หมายความว่าค่าตัวเลขเป็นเพียงการประมาณปริมาณทางกายภาพบางอย่างที่ทำแบบจำลอง
สตีเฟ่น Canon

10
สำหรับการวิเคราะห์เชิงตัวเลขสมองของคุณจะขอบคุณถ้าคุณตีความตัวเลขทศนิยมไม่ได้เป็นช่วงเวลา แต่เป็นค่าที่แน่นอน (ซึ่งเกิดขึ้นจะไม่ตรงกับค่าที่คุณต้องการ) ตัวอย่างเช่นหาก x เป็นบางรอบ 4.5 ที่มีข้อผิดพลาดน้อยกว่า 0.1 และคุณคำนวณ (x + 1) - x การตีความ "ช่วงเวลา" จะทำให้คุณมีช่วงจาก 0.8 ถึง 1.2 ในขณะที่การตีความ "ค่าที่แน่นอน" บอก คุณจะได้ผลลัพธ์เป็น 1 โดยมีข้อผิดพลาดมากที่สุด 2 ^ (- 50) ด้วยความแม่นยำสองเท่า
gnasher729

34

ยังไม่มีผู้โพสต์ที่กล่าวถึงการหดตัวของนิพจน์ลอยตัว (มาตรฐาน ISO C, 6.5p8 และ 7.12.2) หากว่าFP_CONTRACTตั้งค่า pragma ไว้ONคอมไพเลอร์จะได้รับอนุญาตให้พิจารณานิพจน์เช่นa*a*a*a*a*aการดำเนินการเดียวราวกับว่าประเมินด้วยการปัดเศษเพียงครั้งเดียว ตัวอย่างเช่นคอมไพเลอร์อาจแทนที่ด้วยฟังก์ชันพลังงานภายในที่ทั้งเร็วขึ้นและแม่นยำยิ่งขึ้น สิ่งนี้น่าสนใจเป็นพิเศษเนื่องจากพฤติกรรมถูกควบคุมโดยโปรแกรมเมอร์บางส่วนโดยตรงในซอร์สโค้ดในขณะที่บางครั้งตัวเลือกคอมไพเลอร์จากผู้ใช้อาจถูกใช้อย่างไม่ถูกต้อง

สถานะเริ่มต้นของ FP_CONTRACT pragma ถูกกำหนดโดยการนำไปใช้งานเพื่อให้คอมไพเลอร์ได้รับอนุญาตให้ทำการปรับแต่งเช่นนี้ให้เป็นค่าเริ่มต้น ดังนั้นรหัสพกพาที่ต้องปฏิบัติตามกฎ IEEE 754 อย่างเคร่งครัดควรตั้งเป็นOFFกฎควรกำหนดอย่างชัดเจนว่ามัน

ถ้าคอมไพเลอร์ไม่สนับสนุน pragma OFFนี้มันจะต้องเป็นแบบอนุรักษ์นิยมโดยการหลีกเลี่ยงการเพิ่มประสิทธิภาพดังกล่าวในกรณีที่นักพัฒนาที่ได้เลือกที่จะตั้งค่าให้

GCC ไม่สนับสนุน pragma นี้ แต่มีตัวเลือกเริ่มต้นที่จะอนุมานว่ามันจะON; ดังนั้นสำหรับเป้าหมายที่มีฮาร์ดแวร์ FMA หากต้องการป้องกันการแปลงa*b+cเป็น fma (a, b, c) เราจำเป็นต้องจัดเตรียมตัวเลือกเช่น-ffp-contract=off(เพื่อกำหนด pragma ให้ชัดเจนOFF) หรือ-std=c99(เพื่อบอก GCC ให้สอดคล้องกับบางอย่าง รุ่นมาตรฐาน C ที่นี่ C99 จึงเป็นไปตามย่อหน้าด้านบน) ในอดีตตัวเลือกหลังไม่ได้ป้องกันการเปลี่ยนแปลงซึ่งหมายความว่า GCC ไม่สอดคล้องกับประเด็นนี้: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


3
บางครั้งคำถามยอดนิยมที่มีอายุยืนแสดงให้เห็นอายุของพวกเขา คำถามนี้ถูกถามและตอบในปี 2011 เมื่อ GCC สามารถขอตัวได้โดยไม่เคารพมาตรฐาน C99 ที่ผ่านมา แน่นอนตอนนี้คือปี 2014 ดังนั้น GCC …อะแฮ่ม
Pascal Cuoq

คุณไม่ควรตอบคำถามแบบอิงดัชนีที่ไม่ได้รับคำตอบล่าสุดแทนใช่ไหม? ไอstackoverflow.com/questions/23703408ไอ
ปาสกาล Cuoq

ฉันพบว่า ... รบกวนว่า gcc ไม่ได้ใช้ Pragmas จุดลอยตัว C99
David Monniaux

1
@DavidMonniaux pragmas เป็นคำจำกัดความที่เป็นตัวเลือกในการใช้
Tim Seguine

2
@TimSeguine แต่ถ้าไม่มีการนำ pragma มาใช้ค่าเริ่มต้นจะต้องมีข้อ จำกัด มากที่สุดสำหรับการนำไปใช้ ฉันคิดว่านั่นคือสิ่งที่เดวิดคิด ด้วย GCC ตอนนี้ได้รับการแก้ไขสำหรับ FP_CONTRACT หากใช้โหมด ISO C : มันยังไม่ได้ใช้ pragma แต่ในโหมด ISO C ตอนนี้จะถือว่า pragma ปิดอยู่
vinc17

28

Lambdageek ชี้ให้เห็นว่าการคูณแบบลอยนั้นไม่ได้เชื่อมโยงกันและคุณจะได้รับความแม่นยำน้อยลง แต่เมื่อได้รับความแม่นยำที่ดีกว่าคุณสามารถโต้แย้งการปรับให้เหมาะสมเพราะคุณต้องการแอปพลิเคชันที่กำหนดค่าได้ ตัวอย่างเช่นในไคลเอนต์การจำลองเกม / เซิร์ฟเวอร์ที่ไคลเอนต์ทุกคนต้องจำลองโลกเดียวกันที่คุณต้องการให้การคำนวณจุดลอยตัวเป็นที่แน่นอน


3
@greggo ไม่มันยังคงถูกกำหนดไว้แล้ว ไม่มีการสุ่มเพิ่มในความหมายของคำใด ๆ
อลิซ

9
@ อลิซดูเหมือนว่า Bjorn ค่อนข้างชัดเจนที่นี่ใช้ 'deterministic' ในความรู้สึกของรหัสที่ให้ผลลัพธ์เดียวกันบนแพลตฟอร์มที่แตกต่างกันและคอมไพเลอร์รุ่นอื่น ๆ (ตัวแปรภายนอกซึ่งอาจอยู่นอกเหนือการควบคุมของโปรแกรมเมอร์) - ซึ่งตรงกันข้ามกับการขาด ของการสุ่มตัวเลขที่แท้จริงในขณะใช้งาน หากคุณกำลังชี้ให้เห็นว่านี่ไม่ใช่การใช้คำที่เหมาะสมฉันจะไม่โต้แย้งกับสิ่งนั้น
greggo

5
@greggo ยกเว้นในความหมายของสิ่งที่เขาพูดมันก็ยังผิดอยู่ นั่นคือจุดทั้งหมดของ IEEE 754 เพื่อให้มีคุณสมบัติที่เหมือนกันสำหรับการดำเนินการส่วนใหญ่ (ถ้าไม่ทั้งหมด) ข้ามแพลตฟอร์ม ตอนนี้เขาไม่ได้กล่าวถึงแพลตฟอร์มหรือคอมไพเลอร์ซึ่งจะเป็นข้อกังวลที่ถูกต้องหากคุณต้องการให้ทุกการดำเนินการเดียวในทุก ๆ เซิร์ฟเวอร์ / ไคลเอนต์จากระยะไกลเหมือนกัน .... แต่นี่ไม่ชัดเจนจากคำสั่งของเขา คำที่ดีกว่าอาจเป็น "คล้ายกันอย่างน่าเชื่อถือ" หรือบางสิ่งบางอย่าง
อลิซ

8
@ อลิซคุณกำลังเสียเวลาของทุกคนรวมถึงของคุณเองโดยการโต้แย้งความหมาย ความหมายของเขาชัดเจน
Lanaru

11
@ Lanaru จุดทั้งหมดของมาตรฐานคือความหมาย; ความหมายของเขาไม่ชัดเจน
อลิซ

28

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

การดำเนินงานพื้นฐานดังต่อไปนี้:

pow(x,y);

มีข้อผิดพลาดโดยธรรมชาติที่มีขนาดใกล้เคียงกับข้อผิดพลาดในการคูณหรือการหารเดี่ยวใด ๆขนาดเดียวกับข้อผิดพลาดในการคูณเดียวหรือแบ่ง

ในขณะที่การดำเนินการดังต่อไปนี้:

float a=someValue;
float b=a*a*a*a*a*a;

มีข้อผิดพลาดโดยธรรมชาติที่มากกว่า5 เท่าของข้อผิดพลาดของการคูณเดียวหรือการหาร (เพราะคุณรวม 5 การคูณ)

คอมไพเลอร์ควรจะระมัดระวังกับชนิดของการปรับให้เหมาะสมที่สุดที่กำลังทำอยู่:

  1. ถ้าการเพิ่มประสิทธิภาพpow(a,6)ให้กับa*a*a*a*a*aมันอาจจะปรับปรุงประสิทธิภาพการทำงาน แต่ลดความถูกต้องสำหรับตัวเลขทศนิยม
  2. หากการปรับa*a*a*a*a*a ให้เหมาะสมpow(a,6)อาจลดความถูกต้องได้จริงเพราะ "a" เป็นค่าพิเศษบางอย่างที่ช่วยให้การคูณไม่มีข้อผิดพลาด (กำลัง 2 หรือจำนวนเต็มเล็กน้อย)
  3. หากปรับpow(a,6)ให้เหมาะสม(a*a*a)*(a*a*a)หรือ(a*a)*(a*a)*(a*a)ยังคงมีการสูญเสียความถูกต้องเมื่อเทียบกับpowฟังก์ชั่น

โดยทั่วไปแล้วคุณรู้ว่าสำหรับจุดลอยตัวตามอำเภอใจ "pow" มีความแม่นยำที่ดีกว่าฟังก์ชั่นใด ๆ ที่คุณสามารถเขียนได้ในที่สุด แต่ในบางกรณีพิเศษหลายการคูณอาจมีความแม่นยำและประสิทธิภาพที่ดีขึ้น ในที่สุดการแสดงความคิดเห็นรหัสเพื่อให้ไม่มีใครจะ "เพิ่มประสิทธิภาพ" รหัสนั้น

สิ่งเดียวที่สมเหตุสมผล (ความเห็นส่วนตัวและเห็นได้ชัดว่าเป็นทางเลือกใน GCC ที่ไม่มีการเพิ่มประสิทธิภาพหรือการตั้งค่าสถานะคอมไพเลอร์ใด ๆ ) เพื่อปรับให้เหมาะสมควรแทนที่ "pow (a, 2)" ด้วย "a * a" นั่นเป็นเพียงสิ่งเดียวที่ผู้จำหน่ายคอมไพเลอร์ควรทำ


7
downvoters ควรตระหนักว่าคำตอบนี้ใช้ได้อย่างสมบูรณ์ ฉันสามารถอ้างอิงแหล่งข้อมูลและเอกสารจำนวนมากเพื่อสนับสนุนคำตอบของฉันและฉันอาจเกี่ยวข้องกับความแม่นยำของจุดลอยตัวมากกว่าผู้ลงคะแนนใด ๆ มีความสมเหตุสมผลอย่างสมบูรณ์ใน StackOverflow การเพิ่มข้อมูลที่หายไปซึ่งคำตอบอื่น ๆ ไม่ครอบคลุมดังนั้นควรสุภาพและอธิบายเหตุผลของคุณ
CoffeDeveloper

1
สำหรับฉันแล้วคำตอบของ Stephen Canon ครอบคลุมสิ่งที่คุณพูด คุณดูเหมือนจะยืนยันว่า libms ถูกนำไปใช้กับ splines: โดยทั่วไปแล้วพวกเขาจะใช้การลดอาร์กิวเมนต์ (ขึ้นอยู่กับฟังก์ชันที่ใช้) บวกพหุนามเดียวกับค่าสัมประสิทธิ์ที่ได้รับจากตัวแปรที่ซับซ้อนมากขึ้นของอัลกอริทึม Remez ความนุ่มนวลที่จุดเชื่อมต่อนั้นไม่ถือว่าเป็นวัตถุประสงค์ที่คุ้มค่าสำหรับฟังก์ชั่น libm (ถ้ามันถูกต้องมากพอพวกมันจะค่อนข้างราบรื่นโดยอัตโนมัติโดยไม่คำนึงว่าจะแบ่งโดเมนออกเป็นกี่ส่วน)
Pascal Cuoq

ในช่วงครึ่งหลังของคำตอบของคุณโดยสิ้นเชิงพลาดจุดที่คอมไพเลอร์ควรจะผลิตรหัสที่ใช้สิ่งที่ซอร์สโค้ดบอกว่าจุด นอกจากนี้คุณใช้คำว่า "ความแม่นยำ" เมื่อคุณหมายถึง "ความถูกต้อง"
Pascal Cuoq

ขอบคุณสำหรับการป้อนข้อมูลของคุณฉันแก้ไขคำตอบเล็กน้อยมีอะไรใหม่ยังคงปรากฏอยู่ใน 2 บรรทัดสุดท้าย ^^
CoffeDeveloper

27

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

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

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

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


17
ตามที่ระบุไว้โดยผู้แสดงความคิดเห็นอื่นนี้เป็นเรื่องจริงถึงจุดที่ไร้สาระ; ความแตกต่างอาจมากถึงครึ่งหนึ่งของค่าใช้จ่าย 10% และหากทำงานแบบวนรอบอย่างแน่นหนานั่นจะแปลคำแนะนำมากมายที่เสียไปเพื่อให้ได้สิ่งที่อาจมีความแม่นยำเพิ่มขึ้นเล็กน้อย การบอกว่าคุณไม่ควรใช้ FP มาตรฐานเมื่อคุณทำ monte carlo นั้นเหมือนกับว่าคุณควรใช้เครื่องบินเพื่อเดินทางข้ามประเทศ มันไม่สนใจสิ่งภายนอกจำนวนมาก ท้ายที่สุดนี่ไม่ใช่การเพิ่มประสิทธิภาพที่ผิดปกติ การวิเคราะห์รหัสตายและการลดรหัส / refactor เป็นเรื่องธรรมดามาก
อลิซ

21

มีคำตอบที่ดีอยู่เล็กน้อยสำหรับคำถามนี้ แต่เพื่อความสมบูรณ์ฉันต้องการชี้ให้เห็นว่าส่วนที่บังคับใช้ของมาตรฐาน C คือ 5.1.2.2.3 / 15 (ซึ่งเหมือนกับมาตรา 1.9 / 9 ใน มาตรฐาน C ++ 11) ส่วนนี้ระบุว่าโอเปอเรเตอร์สามารถจัดกลุ่มใหม่ได้หากมีการเชื่อมโยงหรือสับเปลี่ยนจริงๆ


12

gcc จริง ๆ แล้วสามารถทำการเพิ่มประสิทธิภาพนี้ได้แม้จะเป็นเลขทศนิยมก็ตาม ตัวอย่างเช่น,

double foo(double a) {
  return a*a*a*a*a*a;
}

กลายเป็น

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

-O -funsafe-math-optimizationsกับ การเรียงลำดับใหม่นี้ละเมิด IEEE-754 ดังนั้นจึงจำเป็นต้องมีการตั้งค่าสถานะ

เลขจำนวนเต็มที่ลงนามตามที่ Peter Cordes ชี้ให้เห็นในความคิดเห็นสามารถทำการเพิ่มประสิทธิภาพนี้ได้โดยไม่ต้อง-funsafe-math-optimizationsมีการเก็บรักษาอย่างแน่นอนเมื่อไม่มีการล้นและหากมีการล้นคุณจะได้รับพฤติกรรมที่ไม่ได้กำหนด ดังนั้นคุณจะได้รับ

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

-Oมีเพียง สำหรับเลขจำนวนเต็มที่ไม่ได้ลงนามมันง่ายยิ่งกว่าเพราะมันทำงานกับ mod mod ที่ 2 และสามารถจัดลำดับใหม่ได้อย่างอิสระแม้ในหน้าล้น


1
เชื่อมโยง Godboltด้วย double, int และ unsigned GCC และเสียงดังกราวทั้งเพิ่มประสิทธิภาพทั้งสามลักษณะเดียวกัน (กับ-ffast-math)
ปีเตอร์ Cordes

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