เหตุใดการวนซ้ำแบบง่ายจึงได้รับการปรับให้เหมาะสมเมื่อขีด จำกัด คือ 959 แต่ไม่ใช่ 960


131

พิจารณาลูปง่ายๆนี้:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 959; i++)
    p += 1;
  return p;
}

หากคุณคอมไพล์ด้วย gcc 7 (snapshot) หรือ clang (trunk) ด้วย-march=core-avx2 -Ofastคุณจะได้สิ่งที่คล้ายกับ.

.LCPI0_0:
        .long   1148190720              # float 960
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

กล่าวอีกนัยหนึ่งก็คือตั้งค่าคำตอบเป็น 960 โดยไม่ต้องวนซ้ำ

อย่างไรก็ตามหากคุณเปลี่ยนรหัสเป็น:

float f(float x[]) {
  float p = 1.0;
  for (int i = 0; i < 960; i++)
    p += 1;
  return p;
}

แอสเซมบลีที่ผลิตทำผลรวมลูปจริงหรือ? ตัวอย่างเสียงดังให้:

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   1086324736              # float 6
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        vxorps  ymm1, ymm1, ymm1
        mov     eax, 960
        vbroadcastss    ymm2, dword ptr [rip + .LCPI0_1]
        vxorps  ymm3, ymm3, ymm3
        vxorps  ymm4, ymm4, ymm4
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        vaddps  ymm0, ymm0, ymm2
        vaddps  ymm1, ymm1, ymm2
        vaddps  ymm3, ymm3, ymm2
        vaddps  ymm4, ymm4, ymm2
        add     eax, -192
        jne     .LBB0_1
        vaddps  ymm0, ymm1, ymm0
        vaddps  ymm0, ymm3, ymm0
        vaddps  ymm0, ymm4, ymm0
        vextractf128    xmm1, ymm0, 1
        vaddps  ymm0, ymm0, ymm1
        vpermilpd       xmm1, xmm0, 1   # xmm1 = xmm0[1,0]
        vaddps  ymm0, ymm0, ymm1
        vhaddps ymm0, ymm0, ymm0
        vzeroupper
        ret

เหตุใดจึงเป็นเช่นนั้นและทำไมเสียงดังและ gcc จึงเหมือนกันทุกประการ


ขีด จำกัด ของลูปเดียวกันหากคุณแทนที่floatด้วยdoubleคือ 479 ซึ่งจะเหมือนกันสำหรับ gcc และเสียงดังอีกครั้ง

อัปเดต 1

ปรากฎว่า gcc 7 (snapshot) และ clang (trunk) ทำงานแตกต่างกันมาก clang ปรับลูปให้เหมาะสมสำหรับขีด จำกัด ทั้งหมดที่น้อยกว่า 960 เท่าที่ฉันสามารถบอกได้ ในทางกลับกัน gcc มีความอ่อนไหวต่อค่าที่แน่นอนและไม่มีขีด จำกัด บน ตัวอย่างเช่นจะไม่เพิ่มประสิทธิภาพของลูปเมื่อขีด จำกัด คือ 200 (รวมถึงค่าอื่น ๆ อีกมากมาย) แต่จะทำเมื่อขีด จำกัด คือ 202 และ 20002 (รวมถึงค่าอื่น ๆ อีกมากมาย)


3
สิ่งที่ Sulthan อาจหมายถึงคือ 1) คอมไพเลอร์คลายการวนซ้ำและ 2) เมื่อคลายการควบคุมแล้วจะเห็นว่าการดำเนินการ sum สามารถรวมเป็นหนึ่งได้ หากไม่ได้คลายการวนซ้ำจะไม่สามารถจัดกลุ่มการดำเนินการได้
Jean-François Fabre

3
การมีลูปจำนวนคี่ทำให้การคลายความซับซ้อนมากขึ้นการทำซ้ำสองสามครั้งสุดท้ายจะต้องทำเป็นพิเศษ นั่นอาจเพียงพอที่จะทำให้เครื่องมือเพิ่มประสิทธิภาพเข้าสู่โหมดที่ไม่สามารถจดจำทางลัดได้อีกต่อไป เป็นไปได้ค่อนข้างมากก่อนอื่นต้องเพิ่มรหัสสำหรับกรณีพิเศษจากนั้นจะต้องลบออกอีกครั้ง การใช้เครื่องมือเพิ่มประสิทธิภาพระหว่างหูจะดีที่สุดเสมอ :)
Hans Passant

3
@HansPassant นอกจากนี้ยังเหมาะสำหรับหมายเลขใด ๆ ที่มีขนาดเล็กกว่า 959
eleanora

6
โดยปกติแล้วสิ่งนี้จะไม่สามารถทำได้ด้วยการกำจัดตัวแปรเหนี่ยวนำแทนที่จะคลายจำนวนที่บ้า? การคลายตัวด้วยตัวประกอบ 959 นั้นบ้ามาก
harold

4
@eleanora ฉันเล่นกับ compilre explorer และสิ่งต่อไปนี้ดูเหมือนจะค้างไว้ (พูดถึงสแนปชอต gcc เท่านั้น): หากจำนวนลูปเป็นผลคูณของ 4 และอย่างน้อย 72 ลูปจะไม่ถูกคลายการควบคุม (หรือมากกว่านั้นไม่ได้รับการควบคุมโดย a ปัจจัย 4); มิฉะนั้นลูปทั้งหมดจะถูกแทนที่ด้วยค่าคงที่แม้ว่าจำนวนลูปจะเป็น 2000000001 ก็ตามความสงสัยของฉัน: การเพิ่มประสิทธิภาพก่อนกำหนด (เช่นเดียวกับ "เฮ้ก่อนหน้านี้ผลคูณของ 4 ดีสำหรับการคลาย" ซึ่งบล็อกการเพิ่มประสิทธิภาพเพิ่มเติมเทียบกับ a อย่างละเอียดถี่ถ้วนมากขึ้น "ข้อตกลงกับลูปนี้คืออะไร")
Hagen von Eitzen

คำตอบ:


88

TL; DR

ตามค่าเริ่มต้นสแน็ปช็อตปัจจุบัน GCC 7 จะทำงานไม่สอดคล้องกันในขณะที่เวอร์ชันก่อนหน้ามีขีด จำกัด เริ่มต้นเนื่องจากPARAM_MAX_COMPLETELY_PEEL_TIMES16 ซึ่งสามารถแทนที่ได้จากบรรทัดคำสั่ง

เหตุผลของวงเงินคือการป้องกันไม่ให้คลี่ห่วงก้าวร้าวเกินไปที่สามารถเป็นดาบสองคม

GCC เวอร์ชัน <= 6.3.0

ตัวเลือกการเพิ่มประสิทธิภาพที่เกี่ยวข้องสำหรับ GCC คือ-fpeel-loopsซึ่งเปิดใช้งานทางอ้อมพร้อมกับแฟล็ก-Ofast(การเน้นเป็นของฉัน):

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

เปิดใช้งานด้วยและ-O3 / หรือ-fprofile-use

สามารถรับรายละเอียดเพิ่มเติมได้โดยเพิ่ม-fdump-tree-cunroll:

$ head test.c.151t.cunroll 

;; Function f (f, funcdef_no=0, decl_uid=1919, cgraph_uid=0, symbol_order=0)

Not peeling: upper bound is known so can unroll completely

ข้อความมาจาก/gcc/tree-ssa-loop-ivcanon.c:

if (maxiter >= 0 && maxiter <= npeel)
    {
      if (dump_file)
        fprintf (dump_file, "Not peeling: upper bound is known so can "
         "unroll completely\n");
      return false;
    }

จึงกลับมาทำงานtry_peel_loopfalse

สามารถเข้าถึงเอาต์พุต verbose เพิ่มเติมได้ด้วย-fdump-tree-cunroll-details:

Loop 1 iterates 959 times.
Loop 1 iterates at most 959 times.
Not unrolling loop 1 (--param max-completely-peeled-times limit reached).
Not peeling: upper bound is known so can unroll completely

เป็นไปได้ที่จะปรับแต่งขีด จำกัด โดยการเชื่อมต่อด้วยmax-completely-peeled-insns=nและmax-completely-peel-times=nparams:

max-completely-peeled-insns

จำนวนอินส์สูงสุดของลูปที่ลอกออกทั้งหมด

max-completely-peel-times

จำนวนการวนซ้ำสูงสุดของลูปที่เหมาะสมสำหรับการลอกแบบสมบูรณ์

ต้องการเรียนรู้เพิ่มเติมเกี่ยวกับ insns คุณสามารถดูคู่มือการใช้งาน GCC Internals

ตัวอย่างเช่นหากคุณรวบรวมด้วยตัวเลือกต่อไปนี้:

-march=core-avx2 -Ofast --param max-completely-peeled-insns=1000 --param max-completely-peel-times=1000

จากนั้นรหัสจะเปลี่ยนเป็น:

f:
        vmovss  xmm0, DWORD PTR .LC0[rip]
        ret
.LC0:
        .long   1148207104

เสียงดังกราว

ฉันไม่แน่ใจว่าจริง ๆ แล้ว Clang ทำอะไรและจะปรับแต่งขีด จำกัด ของมันอย่างไร แต่อย่างที่ฉันสังเกตคุณสามารถบังคับให้มันประเมินค่าสุดท้ายได้โดยทำเครื่องหมายลูปด้วยunroll pragmaและมันจะลบออกทั้งหมด:

#pragma unroll
for (int i = 0; i < 960; i++)
    p++;

ผลลัพธ์เป็น:

.LCPI0_0:
        .long   1148207104              # float 961
f:                                      # @f
        vmovss  xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
        ret

ขอบคุณสำหรับคำตอบที่ดีนี้ ดังที่คนอื่น ๆ ชี้ให้เห็นว่า gcc ดูเหมือนจะอ่อนไหวต่อขนาดขีด จำกัด ที่แน่นอน ยกตัวอย่างเช่นมันล้มเหลวที่จะกำจัดห่วงสำหรับ 912 godbolt.org/g/EQJHvT fdump-tree-cunroll-details พูดอะไรในกรณีนั้น?
eleanora

ในความเป็นจริงแม้แต่ 200 ก็มีปัญหานี้ ทั้งหมดนี้เป็นภาพรวมของ gcc 7 ที่ Godbolt จัดเตรียมไว้ให้ godbolt.org/g/Vg3SVs สิ่งนี้ไม่ได้ใช้กับเสียงดังเลย
eleanora

13
คุณอธิบายกลไกของการลอกออก แต่ไม่ทราบว่าความเกี่ยวข้องของ 960 คืออะไรหรือทำไมถึงมีขีด จำกัด เลย
MM

1
@MM: พฤติกรรมการลอกแตกต่างกันอย่างสิ้นเชิงระหว่าง GCC 6.3.0 และ snaphost ล่าสุด ในกรณีก่อนหน้านี้ฉันสงสัยเป็นอย่างยิ่งว่าการ จำกัด ฮาร์ดโค้ดถูกบังคับใช้โดยPARAM_MAX_COMPLETELY_PEEL_TIMESพารามิเตอร์ซึ่งกำหนด/gcc/params.def:321ด้วยค่า 16
Grzegorz Szpetkowski

14
คุณอาจต้องการพูดถึงสาเหตุที่ GCC จงใจ จำกัด ตัวเองด้วยวิธีนี้ โดยเฉพาะอย่างยิ่งถ้าคุณคลายลูปอย่างก้าวร้าวเกินไปไบนารีจะใหญ่ขึ้นและคุณมีโอกาสน้อยที่จะพอดีกับแคช L1 การพลาดแคชอาจมีราคาค่อนข้างแพงเมื่อเทียบกับการบันทึกการกระโดดแบบมีเงื่อนไขบางอย่างโดยถือว่าการทำนายสาขาที่ดี (ซึ่งคุณจะมีสำหรับการวนซ้ำทั่วไป)
Kevin

19

หลังจากอ่านความคิดเห็นของ Sulthan ฉันเดาว่า:

  1. คอมไพเลอร์จะคลายลูปอย่างเต็มที่หากตัวนับลูปคงที่ (และไม่สูงเกินไป)

  2. เมื่อยกเลิกการควบคุมแล้วคอมไพลเลอร์จะเห็นว่าการดำเนินการรวมสามารถรวมเป็นหนึ่งเดียวได้

หากลูปไม่ถูกยกเลิกด้วยเหตุผลบางประการ (ที่นี่: จะสร้างคำสั่งมากเกินไปด้วย1000) จะไม่สามารถจัดกลุ่ม

คอมไพลเลอร์จะเห็นว่าการยกเลิกการควบคุมคำสั่ง 1,000 คำสั่งเป็นการเพิ่มเพียงครั้งเดียว แต่ขั้นตอนที่ 1 และ 2 ที่อธิบายไว้ข้างต้นเป็นการเพิ่มประสิทธิภาพแยกกันสองรายการดังนั้นจึงไม่สามารถรับ "ความเสี่ยง" ในการคลายการควบคุมโดยไม่ทราบว่าสามารถจัดกลุ่มการดำเนินการได้หรือไม่ (ตัวอย่าง: ไม่สามารถจัดกลุ่มการเรียกใช้ฟังก์ชันได้)

หมายเหตุ: นี่เป็นกรณีมุมใครใช้การวนซ้ำเพื่อเพิ่มสิ่งเดียวกันซ้ำอีกครั้ง ในกรณีนั้นอย่าพึ่งพาคอมไพเลอร์ที่เป็นไปได้ในการ unroll / optimize; เขียนการดำเนินการที่เหมาะสมโดยตรงในคำสั่งเดียว


1
แล้วคุณสามารถมุ่งเน้นไปที่not too highส่วนนั้นได้หรือไม่? ฉันหมายความว่าทำไมไม่มีความเสี่ยงในกรณีนี้100? ฉันเดาอะไรบางอย่างได้ ... ในความคิดเห็นของฉันข้างบน.. มันอาจเป็นเหตุผลได้หรือไม่?
user2736738

ฉันคิดว่าคอมไพเลอร์ไม่ได้ตระหนักถึงความไม่ถูกต้องของจุดลอยตัวที่อาจทำให้เกิดขึ้นได้ ฉันเดาว่ามันเป็นเพียงการ จำกัด ขนาดคำสั่ง คุณได้max-unrolled-insnsเคียงข้างmax-unrolled-times
Jean-François Fabre

มันเป็นความคิดหรือการคาดเดาของฉัน ... ขอให้ได้เหตุผลที่ชัดเจนมากขึ้น
user2736738

5
สิ่งที่น่าสนใจคือถ้าคุณเปลี่ยนfloatเป็นintคอมไพเลอร์ gcc สามารถลดความแรงของลูปได้โดยไม่คำนึงถึงจำนวนการวนซ้ำเนื่องจากการเพิ่มประสิทธิภาพตัวแปรเหนี่ยวนำ ( -fivopts) แต่สิ่งเหล่านี้ดูเหมือนจะไม่ได้ผลสำหรับfloats
Tavian Barnes

1
@CortAmmon Right และฉันจำได้ว่าอ่านบางคนที่รู้สึกประหลาดใจและไม่พอใจที่ GCC ใช้ MPFR เพื่อคำนวณตัวเลขจำนวนมากอย่างแม่นยำให้ผลลัพธ์ที่ค่อนข้างแตกต่างจากการดำเนินการจุดลอยตัวที่เทียบเท่าซึ่งจะมีข้อผิดพลาดสะสมและการสูญเสียความแม่นยำ แสดงให้เห็นว่าหลายคนคำนวณทศนิยมผิดวิธี
Zan Lynx

12

คำถามดีมาก!

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

นอกจากนี้คุณยังสามารถเล่นกับCompiler Explorer ของ Godboltเพื่อเปรียบเทียบว่าคอมไพเลอร์และตัวเลือกต่างๆส่งผลต่อโค้ดที่สร้างขึ้นอย่างไรgcc 6.2และicc 17ยังคงแทรกโค้ดเป็น 960 ในขณะที่clang 3.9ไม่ได้ (ด้วยการกำหนดค่าเริ่มต้นของ Godbolt จะหยุดการอินไลน์ที่ 73)


ฉันได้แก้ไขคำถามเพื่อให้ชัดเจนถึงเวอร์ชันของ gcc และ clang ที่ฉันใช้ ดูgodbolt.org/g/FfwWjL ฉันใช้ -Ofast เช่น
eleanora
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.