คือเร็วกว่าif( a < 901 )
if( a <= 900 )
ไม่เหมือนกับตัวอย่างง่ายๆนี้ แต่มีการเปลี่ยนแปลงประสิทธิภาพเล็กน้อยในรหัสซับซ้อนของลูป ฉันคิดว่ามันต้องทำอะไรบางอย่างกับรหัสเครื่องที่สร้างขึ้นในกรณีที่มันเป็นจริง
<
<=
คือเร็วกว่าif( a < 901 )
if( a <= 900 )
ไม่เหมือนกับตัวอย่างง่ายๆนี้ แต่มีการเปลี่ยนแปลงประสิทธิภาพเล็กน้อยในรหัสซับซ้อนของลูป ฉันคิดว่ามันต้องทำอะไรบางอย่างกับรหัสเครื่องที่สร้างขึ้นในกรณีที่มันเป็นจริง
<
<=
คำตอบ:
ไม่มันจะไม่เร็วขึ้นสำหรับสถาปัตยกรรมส่วนใหญ่ คุณไม่ได้ระบุ แต่ใน x86 การเปรียบเทียบแบบครบถ้วนทั้งหมดจะถูกนำไปใช้ในคำสั่งเครื่องสองคำ:
test
หรือcmp
การเรียนการสอนซึ่งชุดEFLAGS
Jcc
คำสั่ง (กระโดด)ขึ้นอยู่กับประเภทการเปรียบเทียบ (และเลย์เอาต์โค้ด):
jne
- กระโดดถ้าไม่เท่ากับ -> ZF = 0
jz
- กระโดดถ้าศูนย์ (เท่ากับ) -> ZF = 1
jg
- กระโดดถ้ายิ่งใหญ่กว่า -> ZF = 0 and SF = OF
ตัวอย่าง (แก้ไขเพื่อความกะทัดรัด) รวบรวมด้วย$ gcc -m32 -S -masm=intel test.c
if (a < b) {
// Do something 1
}
รวบรวมเพื่อ:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jge .L2 ; jump if a is >= b
; Do something 1
.L2:
และ
if (a <= b) {
// Do something 2
}
รวบรวมเพื่อ:
mov eax, DWORD PTR [esp+24] ; a
cmp eax, DWORD PTR [esp+28] ; b
jg .L5 ; jump if a is > b
; Do something 2
.L5:
ดังนั้นข้อแตกต่างระหว่างสองอย่างนี้ก็คือjg
เมื่อเทียบกับjge
คำสั่ง ทั้งสองจะใช้เวลาเท่ากัน
ฉันต้องการพูดถึงความคิดเห็นที่ไม่มีอะไรบ่งบอกว่าคำแนะนำการกระโดดที่แตกต่างกันใช้เวลาเท่ากัน อันนี้ค่อนข้างยากที่จะตอบ แต่นี่คือสิ่งที่ฉันสามารถให้ได้: ในการอ้างอิงชุดคำสั่ง Intelพวกเขาทั้งหมดจะถูกจัดกลุ่มเข้าด้วยกันภายใต้คำสั่งทั่วไปเดียวJcc
(กระโดดถ้าตรงตามเงื่อนไข) การจัดกลุ่มเดียวกันทำร่วมกันภายใต้คู่มืออ้างอิงการปรับให้เหมาะสมในภาคผนวก C. เวลาแฝงและปริมาณงาน
Latency - จำนวนรอบสัญญาณนาฬิกาที่จำเป็นสำหรับแกนประมวลผลเพื่อให้การประมวลผลของμopsทั้งหมดเป็นรูปแบบคำสั่งเสร็จสมบูรณ์
ปริมาณงาน - จำนวนรอบสัญญาณนาฬิกาที่จำเป็นต้องรอก่อนที่พอร์ตปัญหาจะมีอิสระที่จะยอมรับคำสั่งเดิมอีกครั้ง สำหรับคำสั่งจำนวนมากปริมาณงานของคำสั่งอาจน้อยกว่าความหน่วงแฝง
ค่าสำหรับJcc
คือ:
Latency Throughput
Jcc N/A 0.5
ด้วยเชิงอรรถต่อไปนี้บนJcc
:
7) การเลือกคำแนะนำการกระโดดแบบมีเงื่อนไขควรเป็นไปตามคำแนะนำของส่วน 3.4.1, "การคาดคะเนสาขา" เพื่อปรับปรุงความสามารถในการคาดการณ์ของสาขา เมื่อการทำนายกิ่งประสบความสำเร็จเวลาแฝงของ
jcc
จะเป็นศูนย์ได้อย่างมีประสิทธิภาพ
ดังนั้นไม่มีสิ่งใดในเอกสารของ Intel ที่จะปฏิบัติต่อJcc
คำสั่งหนึ่งที่แตกต่างไปจากเอกสารอื่น ๆ
ถ้าใครคิดเกี่ยวกับวงจรจริงที่ใช้ในการทำตามคำแนะนำเราสามารถสันนิษฐานได้ว่าจะมีประตู AND / หรือง่าย ๆ บนบิตต่างEFLAGS
ๆ เพื่อตรวจสอบว่าเป็นไปตามเงื่อนไขหรือไม่ ดังนั้นไม่มีเหตุผลว่าคำสั่งการทดสอบสองบิตควรใช้เวลามากกว่าหรือน้อยกว่าการทดสอบเพียงครั้งเดียว (ละเว้นการหน่วงเวลาการแพร่กระจายเกตซึ่งน้อยกว่าระยะเวลาของนาฬิกามาก)
แก้ไข: Floating Point
สิ่งนี้ถือเป็นจริงสำหรับจุดลอยตัว x87 เช่นกัน: (รหัสเดียวกันสวยมากด้านบน แต่double
แทนที่จะเป็นint
)
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
fstp st(0)
seta al ; Set al if above (CF=0 and ZF=0).
test al, al
je .L2
; Do something 1
.L2:
fld QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; (same thing as above)
fstp st(0)
setae al ; Set al if above or equal (CF=0).
test al, al
je .L5
; Do something 2
.L5:
leave
ret
jg
และjnle
เป็นคำสั่งเดียวกัน7F
:-)
ในอดีต (เรากำลังพูดถึงปี 1980 และต้นปี 1990) มีสถาปัตยกรรมบางอย่างที่สิ่งนี้เป็นจริง ปัญหาหลักคือการเปรียบเทียบจำนวนเต็มถูกนำมาใช้อย่างแท้จริงผ่านการลบจำนวนเต็ม สิ่งนี้ก่อให้เกิดกรณีต่อไปนี้
Comparison Subtraction
---------- -----------
A < B --> A - B < 0
A = B --> A - B = 0
A > B --> A - B > 0
ตอนนี้เมื่อA < B
การลบต้องยืมบิตสูงเพื่อให้การลบนั้นถูกต้องเหมือนกับที่คุณพกพาและยืมเมื่อบวกและลบด้วยมือ บิตที่ "ยืม" นี้มักถูกเรียกว่าบิตพกพาและสามารถทดสอบได้โดยคำสั่งสาขา บิตที่สองที่เรียกว่าzero bitจะถูกตั้งค่าหากการลบเป็นศูนย์เหมือนกันซึ่งบ่งบอกถึงความเท่าเทียมกัน
โดยปกติจะมีคำสั่งสาขาที่มีเงื่อนไขอย่างน้อยสองคำสั่งหนึ่งคำสั่งต่อสาขาในบิตนำติดตัวและอีกใบหนึ่งบนศูนย์บิต
ทีนี้เพื่อให้ได้ใจความสำคัญลองขยายตารางก่อนหน้าเพื่อรวมผลลัพธ์ของการถือและศูนย์บิต
Comparison Subtraction Carry Bit Zero Bit
---------- ----------- --------- --------
A < B --> A - B < 0 0 0
A = B --> A - B = 0 1 1
A > B --> A - B > 0 1 0
ดังนั้นการใช้สาขาสำหรับA < B
สามารถทำได้ในหนึ่งคำสั่งเพราะบิตพกพามีความชัดเจนเฉพาะในกรณีนี้นั่นคือ
;; Implementation of "if (A < B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; Branch if Carry is Zero to the new address
แต่ถ้าเราต้องการทำการเปรียบเทียบที่น้อยกว่าหรือเท่ากับเราต้องทำการตรวจสอบเพิ่มเติมของค่าสถานะศูนย์เพื่อตรวจสอบกรณีของความเท่าเทียมกัน
;; Implementation of "if (A <= B) goto address;"
cmp A, B ;; compare A to B
bcz address ;; branch if A < B
bzs address ;; also, Branch if the Zero bit is Set
ดังนั้นในบางเครื่องโดยใช้ "น้อยกว่า" การเปรียบเทียบอาจบันทึกคำสั่งเครื่องหนึ่ง สิ่งนี้มีความเกี่ยวข้องในยุคของความเร็วหน่วยประมวลผลย่อยเมกะเฮิรตซ์และอัตราส่วนความเร็วซีพียูต่อหน่วยความจำ 1: 1 แต่แทบจะไม่เกี่ยวข้องเลยในวันนี้
jge
ซึ่งทดสอบทั้งแฟล็ก zero และ sign / carry
<=
ทดสอบสามารถดำเนินการในหนึ่งในการเรียนการสอนที่มีการแลกเปลี่ยนตัวถูกดำเนินการและการทดสอบสำหรับnot <
(เทียบเท่า>=
) นี้เป็นที่ต้องการด้วยตัวถูกดำเนินการสลับ:<=
cmp B,A; bcs addr
นั่นเป็นเหตุผลที่ทำให้การทดสอบนี้ถูกละเว้นโดย Intel พวกเขาคิดว่ามันซ้ำซ้อนและคุณไม่สามารถซื้อคำแนะนำที่ซ้ำซ้อนได้ในเวลานั้น :-)
สมมติว่าเรากำลังพูดถึงประเภทจำนวนเต็มภายในไม่มีวิธีที่เป็นไปได้ที่อาจเร็วกว่าอีกประเภทหนึ่ง เห็นได้ชัดว่าพวกมันเหมือนกันในเชิงความหมาย พวกเขาทั้งสองขอให้คอมไพเลอร์ทำอย่างเดียวกัน คอมไพเลอร์ที่แตกหักอย่างน่ากลัวเท่านั้นที่จะสร้างโค้ดที่ด้อยกว่าสำหรับหนึ่งในนั้น
ถ้ามีบางแพลตฟอร์มที่<
เร็วกว่า<=
ชนิดจำนวนเต็มง่ายเรียบเรียงควรเสมอแปลง<=
ไป<
สำหรับค่าคงที่ คอมไพเลอร์ใด ๆ ที่ไม่เพียง แต่จะเป็นคอมไพเลอร์ที่ไม่ดี (สำหรับแพลตฟอร์มนั้น)
<
มิได้<=
มีความเร็วจนกว่าคอมไพเลอร์ตัดสินใจที่ความเร็วพวกเขาจะมี นี่คือการเพิ่มประสิทธิภาพที่ง่ายมากสำหรับคอมไพเลอร์เมื่อคุณพิจารณาว่าพวกเขาโดยทั่วไปแล้วดำเนินการเพิ่มประสิทธิภาพรหัสตายเพิ่มประสิทธิภาพโทรหางห่วงรอก (และ unrolling ในโอกาส) parallelisation อัตโนมัติของลูปต่างๆ ฯลฯ ... ทำไมเสียเวลาขบคิด optimisations ก่อนวัยอันควร ? รับการทำงานต้นแบบสร้างโปรไฟล์เพื่อกำหนดว่าการเพิ่มประสิทธิภาพที่สำคัญที่สุดอยู่ที่ใดดำเนินการปรับให้เหมาะสมตามลำดับความสำคัญและโปรไฟล์อีกครั้งตามวิธีการวัดความก้าวหน้า ...
(a < C)
เป็น(a <= C-1)
(สำหรับค่าคงที่บางค่าC
) ทำให้C
การเข้ารหัสในชุดคำสั่งนั้นยากขึ้น ตัวอย่างเช่นชุดคำสั่งอาจสามารถแสดงค่าคงที่ที่ลงนามแล้วจาก -127 ถึง 128 ในรูปแบบกะทัดรัดในการเปรียบเทียบ แต่ค่าคงที่นอกช่วงนั้นต้องโหลดโดยใช้การเข้ารหัสที่ยาวกว่าช้าลงหรือการสอนอื่นทั้งหมด ดังนั้นการเปรียบเทียบเช่น(a < -127)
นั้นอาจไม่มีการเปลี่ยนแปลงอย่างตรงไปตรงมา
a > 127
ไปa > 128
เพราะคุณไม่มีทางเลือกมีคุณใช้หนึ่งที่คุณต้องการ เรากำลังเปรียบเทียบa > 127
กับa >= 128
ซึ่งไม่สามารถต้องการการเข้ารหัสที่แตกต่างกันหรือคำแนะนำที่แตกต่างกันเพราะพวกเขามีตารางความจริงเดียวกัน การเข้ารหัสของอันใดอันหนึ่งเป็นการเข้ารหัสของอีกฝ่ายหนึ่งอย่างเท่าเทียมกัน
<=
เป็น<
ค่าคงที่เสมอ" เท่าที่ฉันรู้การเปลี่ยนแปลงนั้นเกี่ยวข้องกับการเปลี่ยนค่าคงที่ เช่นa <= 42
ถูกรวบรวมa < 43
เพราะ<
เร็วกว่า ในบางกรณีการเปลี่ยนแปลงดังกล่าวจะไม่เกิดผลเพราะค่าคงที่ใหม่อาจต้องการคำแนะนำมากกว่าหรือช้ากว่า แน่นอนa > 127
และa >= 128
เทียบเท่าและคอมไพเลอร์ควรเข้ารหัสทั้งสองแบบในวิธีที่เร็วที่สุด (เหมือนกัน) แต่นั่นไม่ได้ขัดแย้งกับสิ่งที่ฉันพูด
ฉันเห็นว่าไม่เร็วขึ้น คอมไพเลอร์สร้างรหัสเครื่องเดียวกันในแต่ละเงื่อนไขด้วยค่าที่แตกต่างกัน
if(a < 901)
cmpl $900, -4(%rbp)
jg .L2
if(a <=901)
cmpl $901, -4(%rbp)
jg .L3
ตัวอย่างของฉันif
มาจาก GCC บนแพลตฟอร์ม x86_64 บน Linux
นักเขียนคอมไพเลอร์เป็นคนฉลาดและพวกเขาคิดว่าสิ่งเหล่านี้และอื่น ๆ อีกมากมายที่พวกเราส่วนใหญ่ยอมรับ
ฉันสังเกตเห็นว่าถ้ามันไม่คงที่แล้วรหัสเครื่องเดียวกันจะถูกสร้างขึ้นในทั้งสองกรณี
int b;
if(a < b)
cmpl -4(%rbp), %eax
jge .L2
if(a <=b)
cmpl -4(%rbp), %eax
jg .L3
if(a <=900)
แสดงให้เห็นว่าจะสร้างตรง asm เดียวกัน :)
สำหรับรหัสจุดลอยตัว <= การเปรียบเทียบอาจช้ากว่าเดิม (โดยคำสั่งเดียว) แม้ในสถาปัตยกรรมสมัยใหม่ นี่คือฟังก์ชั่นแรก:
int compare_strict(double a, double b) { return a < b; }
บน PowerPC อันดับแรกจะทำการเปรียบเทียบจุดลอยตัว (ซึ่งอัพเดตcr
การลงทะเบียนเงื่อนไข) จากนั้นย้ายการลงทะเบียนเงื่อนไขไปยัง GPR เลื่อนบิต "เปรียบเทียบน้อยกว่า" เข้าที่แล้วส่งกลับ ใช้เวลาสี่คำแนะนำ
ตอนนี้ให้พิจารณาฟังก์ชั่นนี้แทน:
int compare_loose(double a, double b) { return a <= b; }
สิ่งนี้ต้องใช้งานเดียวกันกับcompare_strict
ด้านบน แต่ตอนนี้มีสองบิตที่น่าสนใจ: "น้อยกว่า" และ "เท่ากับ" สิ่งนี้ต้องการคำสั่งพิเศษ ( cror
- เงื่อนไข register bitwise OR) เพื่อรวมสองบิตเหล่านี้เป็นหนึ่งเดียว ดังนั้นcompare_loose
ต้องมีห้าคำแนะนำในขณะที่compare_strict
ต้องการสี่
คุณอาจคิดว่าคอมไพเลอร์สามารถปรับฟังก์ชันที่สองให้เหมาะสมดังนี้:
int compare_loose(double a, double b) { return ! (a > b); }
อย่างไรก็ตามสิ่งนี้จะจัดการกับ NaN ไม่ถูกต้อง NaN1 <= NaN2
และNaN1 > NaN2
จำเป็นต้องประเมินเป็นเท็จทั้งคู่
fucomip
ชุด ZF และ CF
cr
นั้นเทียบเท่ากับค่าสถานะที่เหมือนกับZF
และCF
บน x86 (แม้ว่า CR จะมีความยืดหยุ่นมากกว่า) สิ่งที่ผู้โพสต์กำลังพูดถึงคือการย้ายผลลัพธ์ไปยัง GPR: ซึ่งใช้สองคำสั่งใน PowerPC แต่ x86 มีคำสั่งการย้ายแบบมีเงื่อนไข
บางทีผู้เขียนหนังสือที่ไม่มีชื่อนั้นได้อ่านว่าa > 0
ทำงานได้เร็วกว่าa >= 1
และคิดว่าเป็นเรื่องจริงในระดับสากล
แต่มันเป็นเพราะ0
มีส่วนเกี่ยวข้อง (เพราะCMP
สามารถขึ้นอยู่กับสถาปัตยกรรมแทนที่เช่นกับOR
) <
และไม่ได้เพราะของ
(a >= 1)
ทำงานช้ากว่า(a > 0)
ตั้งแต่อดีตสามารถเปลี่ยนนิด ๆ หลังโดยเพิ่มประสิทธิภาพ ..
อย่างน้อยที่สุดถ้านี่เป็นความจริงคอมไพเลอร์สามารถเพิ่มประสิทธิภาพเล็กน้อย <= b เป็น! (a> b), และดังนั้นแม้ว่าการเปรียบเทียบเองจะช้ากว่าจริง ๆ แต่คอมไพเลอร์ไร้เดียงสาที่สุดคุณจะไม่สังเกตเห็นความแตกต่าง .
NOT
ถูกสร้างขึ้นโดยคำสั่งอื่น ( je
vs. jne
)
พวกเขามีความเร็วเท่ากัน บางทีในสถาปัตยกรรมพิเศษบางอย่างที่เขา / เธอพูดถูกต้อง แต่ในตระกูล x86 อย่างน้อยฉันก็รู้ว่าพวกเขาเหมือนกัน เนื่องจากการทำเช่นนี้ CPU จะทำการลบ (a - b) จากนั้นตรวจสอบการตั้งค่าสถานะของการลงทะเบียนค่าสถานะ บิตสองบิตของรีจิสเตอร์นั้นเรียกว่า ZF (ศูนย์แฟล็ก) และ SF (แฟล็กเครื่องหมาย) และจะทำในรอบเดียวเพราะมันจะทำด้วยการดำเนินการหน้ากาก
สิ่งนี้จะขึ้นอยู่กับสถาปัตยกรรมพื้นฐานที่ C ถูกคอมไพล์ด้วย ตัวประมวลผลและสถาปัตยกรรมบางอย่างอาจมีคำแนะนำที่ชัดเจนว่าเท่ากับหรือน้อยกว่าและเท่ากับซึ่งดำเนินการในจำนวนรอบที่แตกต่างกัน
นั่นอาจจะเป็นเรื่องที่ผิดปกติเพราะคอมไพเลอร์สามารถแก้ไขได้ทำให้มันไม่เกี่ยวข้อง
สำหรับการผสมผสานส่วนใหญ่ของสถาปัตยกรรมคอมไพเลอร์และภาษามันจะไม่เร็วขึ้น
คำตอบอื่น ๆ มีสมาธิกับสถาปัตยกรรมx86และฉันไม่รู้สถาปัตยกรรมARM (ซึ่งแอสเซมเบลอร์ตัวอย่างของคุณน่าจะดี) พอที่จะให้ความเห็นเฉพาะโค้ดที่สร้าง แต่นี่เป็นตัวอย่างของการเพิ่มประสิทธิภาพขนาดเล็กซึ่งเป็นสถาปัตยกรรม เฉพาะและมีแนวโน้มที่จะเป็นการเพิ่มประสิทธิภาพการต่อต้านตามที่จะเป็นการเพิ่มประสิทธิภาพแนวโน้มที่จะเป็นป้องกันการเพิ่มประสิทธิภาพในขณะที่มันคือการที่จะเพิ่มประสิทธิภาพ
ดังนั้นฉันขอแนะนำว่าการเพิ่มประสิทธิภาพขนาดเล็กประเภทนี้เป็นตัวอย่างของลัทธิการขนส่งสินค้าการเขียนโปรแกรมมากกว่าการปฏิบัติทางวิศวกรรมซอฟต์แวร์ที่ดีที่สุด
อาจมีสถาปัตยกรรมบางส่วนที่นี่เป็นการเพิ่มประสิทธิภาพ แต่ฉันรู้ว่าอย่างน้อยหนึ่งสถาปัตยกรรมที่ตรงกันข้ามอาจเป็นจริง สถาปัตยกรรมTransputer ที่น่าเชื่อถือมีเพียงคำแนะนำของรหัสเครื่องที่เท่ากันและมากกว่าหรือเท่ากันดังนั้นการเปรียบเทียบทั้งหมดจะต้องถูกสร้างขึ้นจากแบบดั้งเดิมเหล่านี้
ถึงกระนั้นในเกือบทุกกรณีคอมไพเลอร์สามารถสั่งการประเมินผลในลักษณะที่ในทางปฏิบัติไม่มีการเปรียบเทียบใด ๆ ที่ได้เปรียบกว่าคนอื่น ๆ กรณีที่เลวร้ายที่สุดมันอาจจำเป็นต้องเพิ่มคำสั่งกลับรายการ (REV) เพื่อสลับรายการสองอันดับแรกบนตัวถูกดำเนินการสแต็ก นี่เป็นคำสั่งไบต์เดียวซึ่งใช้รอบเดียวในการรันดังนั้นจึงมีค่าใช้จ่ายน้อยที่สุดเท่าที่จะเป็นไปได้
การปรับให้เหมาะสมขนาดเล็กเช่นนี้เป็นการปรับให้เหมาะสมที่สุดหรือการเพิ่มประสิทธิภาพการต่อต้านขึ้นอยู่กับสถาปัตยกรรมเฉพาะที่คุณใช้ดังนั้นจึงเป็นความคิดที่ดีที่จะเข้าสู่นิสัยของการใช้การปรับให้เหมาะสมกับสถาปัตยกรรมโดยเฉพาะอย่างยิ่งมิฉะนั้นคุณอาจสัญชาตญาณ ใช้หนึ่งเมื่อมันไม่เหมาะสมที่จะทำและดูเหมือนว่านี่คือสิ่งที่หนังสือที่คุณกำลังอ่านคือการสนับสนุน
คุณไม่ควรสังเกตเห็นความแตกต่างแม้ว่าจะมีก็ตาม นอกจากนี้ในทางปฏิบัติคุณจะต้องทำการเพิ่มเติมa + 1
หรือa - 1
ทำให้สภาพคงอยู่เว้นแต่คุณจะใช้ค่าคงที่เวทมนตร์ซึ่งเป็นการปฏิบัติที่แย่มากโดยวิธีการทั้งหมด
คุณสามารถพูดได้ว่าบรรทัดนั้นถูกต้องในภาษาสคริปต์ส่วนใหญ่เนื่องจากอักขระพิเศษส่งผลให้การประมวลผลโค้ดช้าลงเล็กน้อย อย่างไรก็ตามเนื่องจากคำตอบยอดนิยมชี้ให้เห็นว่าควรไม่มีผลใน C ++ และสิ่งใดก็ตามที่ทำด้วยภาษาสคริปต์อาจไม่เกี่ยวข้องกับการปรับให้เหมาะสม
เมื่อผมเขียนคำตอบนี้ผมเป็นเพียงการมองหาที่เกี่ยวกับคำถามชื่อ <เทียบกับ <= โดยทั่วไปไม่ได้เป็นตัวอย่างที่เฉพาะเจาะจงของค่าคงที่เมื่อเทียบกับa < 901
a <= 900
คอมไพเลอร์หลายคนมักจะลดขนาดของค่าคงที่โดยการแปลงระหว่าง<
และ<=
เช่น e เนื่องจาก x86 ตัวถูกดำเนินการทันทีมีการเข้ารหัส 1 ไบต์ที่สั้นกว่าสำหรับ -128..127
สำหรับ ARM และ AArch64 โดยเฉพาะความสามารถในการเข้ารหัสแบบทันทีนั้นขึ้นอยู่กับความสามารถในการหมุนฟิลด์แคบ ๆ ให้อยู่ในตำแหน่งใด ๆ ในคำ ดังนั้นcmp w0, #0x00f000
จะเข้ารหัสได้ในขณะที่cmp w0, #0x00effff
อาจจะไม่ ดังนั้นกฎที่ทำให้เล็กลงสำหรับการเปรียบเทียบกับค่าคงที่เวลาคอมไพล์ไม่ได้ใช้กับ AArch64 เสมอไป
ในภาษาประกอบในเครื่องมากที่สุดสำหรับการเปรียบเทียบมีค่าใช้จ่ายเช่นเดียวกับการเปรียบเทียบหา<=
<
สิ่งนี้นำไปใช้ไม่ว่าคุณจะทำการแยกมันบูลีนเพื่อสร้างจำนวนเต็ม 0/1 หรือใช้เป็นเพรดิเคตสำหรับการดำเนินการเลือกแบบไม่มีสาขา (เช่น x86 CMOV) คำตอบอื่น ๆ ได้กล่าวถึงส่วนนี้ของคำถามเท่านั้น
แต่คำถามนี้เกี่ยวกับตัวดำเนินการ C ++ คืออินพุตของเครื่องมือเพิ่มประสิทธิภาพ โดยปกติพวกเขาทั้งสองมีประสิทธิภาพเท่าเทียมกัน คำแนะนำจากหนังสือเล่มนี้ฟังดูผิด ๆ โดยสิ้นเชิงเพราะคอมไพเลอร์สามารถเปลี่ยนการเปรียบเทียบที่พวกเขาใช้ใน asm แต่มีข้อยกเว้นอย่างน้อยหนึ่งข้อที่การใช้งาน<=
สามารถสร้างบางอย่างที่คอมไพเลอร์ไม่สามารถปรับให้เหมาะสม
เป็นเงื่อนไขห่วงมีกรณีที่<=
เป็นเชิงคุณภาพที่แตกต่างจาก<
เมื่อมันหยุดเรียบเรียงจากการพิสูจน์ว่าห่วงไม่ได้ไม่มีที่สิ้นสุด สิ่งนี้สามารถสร้างความแตกต่างใหญ่ปิดการใช้งานการทำให้เป็นเวกเตอร์อัตโนมัติ
Overflow ที่ไม่ได้ลงนามถูกกำหนดอย่างดีว่าเป็น base-2 ที่ล้อมรอบซึ่งต่างจาก overflow ที่ลงนามแล้ว (UB) โดยทั่วไปแล้วตัวนับลูปที่ลงนามจะปลอดภัยจากสิ่งนี้ด้วยคอมไพเลอร์ที่ปรับให้เหมาะสมตาม UB ที่มีการลงชื่อเกินไม่ได้เกิดขึ้น: ++i <= size
ในที่สุดจะกลายเป็นเท็จเสมอ ( สิ่งที่โปรแกรมเมอร์ C ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด )
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
คอมไพเลอร์สามารถปรับให้เหมาะสมในวิธีที่รักษาพฤติกรรม (ที่กำหนดและสามารถสังเกตได้ตามกฎหมาย) ของแหล่ง C ++ สำหรับค่าอินพุตที่เป็นไปได้ทั้งหมดยกเว้นที่นำไปสู่พฤติกรรมที่ไม่ได้กำหนด
(เรียบง่ายi <= size
จะสร้างปัญหาด้วย แต่ฉันคิดว่าการคำนวณขอบเขตบนเป็นตัวอย่างที่สมจริงยิ่งขึ้นของการแนะนำความเป็นไปได้ของการวนซ้ำไม่สิ้นสุดสำหรับอินพุตที่คุณไม่สนใจ แต่ต้องใช้คอมไพเลอร์พิจารณา)
ในกรณีนี้size=0
นำไปสู่upper_bound=UINT_MAX
และi <= UINT_MAX
เป็นจริงเสมอ ดังนั้นลูปนี้จึงไม่มีที่สิ้นสุดsize=0
และคอมไพเลอร์ต้องเคารพว่าแม้ว่าคุณในฐานะโปรแกรมเมอร์อาจไม่เคยตั้งใจจะส่งผ่าน size = 0 ถ้าคอมไพเลอร์สามารถ inline ฟังก์ชั่นนี้ในการโทรที่มันสามารถพิสูจน์ได้ว่าขนาด = 0 i < size
เป็นไปไม่ได้แล้วที่ดีก็สามารถเพิ่มประสิทธิภาพเหมือนมันสามารถสำหรับ
Asm like if(!size) skip the loop;
do{...}while(--size);
เป็นวิธีหนึ่งที่มีประสิทธิภาพในการเพิ่มประสิทธิfor( i<size )
ภาพลูปถ้าi
ไม่จำเป็นต้องใช้ค่าจริงของลูปในลูป )
แต่นั่นทำ {} ในขณะที่ไม่สามารถสิ้นสุดได้: หากป้อนด้วยsize==0
เราจะได้รับซ้ำ 2 ^ n (การวนซ้ำกับจำนวนเต็มที่ไม่ได้ลงนามทั้งหมดใน for for C ทำให้สามารถแสดงลูปมากกว่าจำนวนเต็มที่ไม่ได้ลงนามทั้งหมดรวมถึงศูนย์ แต่มันไม่ใช่เรื่องง่ายหากไม่มีแฟล็กที่ถือในแบบ asm)
ด้วยการวนซ้ำของตัวนับลูปทำให้มีความเป็นไปได้คอมไพเลอร์สมัยใหม่มักจะ "ยอมแพ้" และไม่ปรับให้เหมาะสมเกือบจะก้าวร้าว
การใช้การi <= n
รับรู้สำนวนเสียงดังกังวานที่ไม่ได้ลงนามของ clang ที่ปรับsum(1 .. n)
ลูปให้เหมาะสมกับรูปแบบปิดตามn * (n+1) / 2
สูตรของ Gauss
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
x86-64 asm จาก clang7.0 และ gcc8.2 บนตัวรวบรวมคอมไพเลอร์ Godbolt
# clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret
แต่สำหรับรุ่นที่ไร้เดียงสาเราเพิ่งได้ลูปลูปจากเสียงดังกราว
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret
GCC ไม่ได้ใช้แบบฟอร์มการปิดทางใดทางหนึ่งดังนั้นทางเลือกของสภาพ loop ไม่ได้จริงๆมันเจ็บปวด ; มันจะปรับเวกเตอร์อัตโนมัติด้วยการเพิ่มจำนวนเต็มของ SIMD โดยรัน 4 i
ค่าแบบขนานในองค์ประกอบของการลงทะเบียน XMM
# "naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something.
มันมีลูปสเกลาร์ธรรมดาซึ่งฉันคิดว่ามันใช้สำหรับขนาดเล็กมากn
และ / หรือสำหรับกรณีลูปไม่สิ้นสุด
BTW, ลูปทั้งสองนี้เสียคำสั่ง (และ uop บน Sandybridge-family CPUs) บนลูปโอเวอร์เฮด sub eax,1
/ jnz
แทนที่จะเป็นadd eax,1
/ cmp / jcc จะมีประสิทธิภาพมากกว่า 1 uop แทน 2 (หลังแมโครฟิวชั่นของ sub / jcc หรือ cmp / jcc) รหัสหลังจากลูปทั้งสองเขียน EAX โดยไม่มีเงื่อนไขดังนั้นจึงไม่ได้ใช้ค่าสุดท้ายของตัวนับลูป
<
<=
แต่แน่นอนว่าtest ecx,ecx
/ bt eax, 3
/ jbe
จะกระโดดถ้าตั้งค่า ZF (ecx == 0) หรือถ้า CF ถูกตั้งค่า (บิต 3 ของ EAX == 1) ทำให้เกิดการตั้งค่าสถานะบางส่วนบน CPU ส่วนใหญ่เนื่องจากค่าสถานะที่อ่านไม่ได้ทั้งหมด มาจากคำสั่งสุดท้ายเพื่อเขียนแฟล็กใด ๆ สำหรับครอบครัว Sandybridge มันไม่ได้หยุดนิ่งจริง ๆ เพียงแค่ต้องการแทรก uop ที่รวมเข้าด้วยกัน cmp
/ test
เขียนการตั้งค่าสถานะทั้งหมด แต่bt
ปล่อยให้ ZF ไม่ได้แก้ไข felixcloutier.com/x86/bt
เฉพาะในกรณีที่ผู้สร้างคอมพิวเตอร์ไม่ดีด้วยตรรกะแบบบูล ซึ่งพวกเขาไม่ควรจะเป็น
ทุกการเปรียบเทียบ ( >=
<=
>
<
) สามารถทำได้ในความเร็วเดียวกัน
การเปรียบเทียบคือการลบ (ความแตกต่าง) และดูว่ามันเป็นบวก / ลบ
(หากmsb
ตั้งค่าไว้หมายเลขจะเป็นลบ)
วิธีการตรวจสอบa >= b
? ย่อยa-b >= 0
ตรวจสอบว่าa-b
เป็นบวก
วิธีการตรวจสอบa <= b
? ย่อย0 <= b-a
ตรวจสอบว่าb-a
เป็นบวก
วิธีการตรวจสอบa < b
? a-b < 0
ตรวจสอบย่อยว่าa-b
เป็นลบหรือไม่
วิธีการตรวจสอบa > b
? 0 > b-a
ตรวจสอบย่อยว่าb-a
เป็นลบหรือไม่
เพียงแค่ใส่คอมพิวเตอร์ก็สามารถทำสิ่งนี้ภายใต้ประทุนสำหรับ op ที่กำหนด:
a >= b
== msb(a-b)==0
a <= b
== msb(b-a)==0
a > b
== msb(b-a)==1
a < b
==msb(a-b)==1
และแน่นอนคอมพิวเตอร์จะไม่ต้องการจริงที่จะทำ==0
หรือ==1
อย่างใดอย่างหนึ่ง
เพราะ==0
มันสามารถย้อนกลับmsb
จากวงจร
อย่างไรก็ตามพวกเขาแน่นอนที่สุดจะไม่a >= b
ถูกคำนวณเป็นa>b || a==b
lol