ทำไม 2 * (i * i) เร็วกว่า 2 * i * i ใน Java


855

โปรแกรม Java ต่อไปนี้ใช้เวลาเฉลี่ยระหว่าง 0.50 วินาทีและ 0.55 วินาทีเพื่อให้ทำงาน:

public static void main(String[] args) {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        n += 2 * (i * i);
    }
    System.out.println((double) (System.nanoTime() - startTime) / 1000000000 + " s");
    System.out.println("n = " + n);
}

หากฉันแทนที่2 * (i * i)ด้วย2 * i * iจะใช้เวลาระหว่าง 0.60 ถึง 0.65 วินาทีในการเรียกใช้ มาทำไม

ฉันรันโปรแกรมแต่ละเวอร์ชัน 15 ครั้งโดยสลับกันระหว่างสองโปรแกรม นี่คือผลลัพธ์:

 2*(i*i)  |  2*i*i
----------+----------
0.5183738 | 0.6246434
0.5298337 | 0.6049722
0.5308647 | 0.6603363
0.5133458 | 0.6243328
0.5003011 | 0.6541802
0.5366181 | 0.6312638
0.515149  | 0.6241105
0.5237389 | 0.627815
0.5249942 | 0.6114252
0.5641624 | 0.6781033
0.538412  | 0.6393969
0.5466744 | 0.6608845
0.531159  | 0.6201077
0.5048032 | 0.6511559
0.5232789 | 0.6544526

วิ่งเร็วที่สุดของการใช้เวลานานกว่าระยะช้าที่สุดของ2 * i * i ถ้าพวกเขามีประสิทธิภาพเดียวกันน่าจะเป็นของที่เกิดขึ้นนี้จะน้อยกว่า2 * (i * i)1/2^15 * 100% = 0.00305%


5
ฉันได้รับผลลัพธ์ที่คล้ายกัน (ตัวเลขแตกต่างกันเล็กน้อย แต่มีช่องว่างที่เห็นได้ชัดเจนและสอดคล้องกันแน่นอนมากกว่าข้อผิดพลาดการสุ่มตัวอย่าง)
Krease


3
@Kasease ดีที่คุณเข้าใจผิด ตามมาตรฐานใหม่ที่ฉันวิ่ง2 * i * iช้ากว่า ฉันจะลองวิ่งกับ Graal เช่นกัน
Jorn Vernee

5
@nullpointer เพื่อค้นหาว่าทำไมตัวจริงถึงเร็วกว่าตัวอื่นเราจะต้องถอดแยกชิ้นส่วนหรือกราฟในอุดมคติสำหรับวิธีการเหล่านั้น แอสเซมเบลอร์น่ารำคาญมากในการลองและคิดออกดังนั้นฉันจึงพยายามที่จะสร้าง debug แบบ OpenJDK ซึ่งสามารถสร้างกราฟที่ดีได้
Jorn Vernee

4
คุณสามารถเปลี่ยนชื่อคำถามของคุณไป " ทำไมเป็นi * i * 2เร็วกว่า2 * i * i? " เพื่อความชัดเจนที่ดีขึ้นว่าปัญหาอยู่ในคำสั่งของการดำเนินงาน
Cœur

คำตอบ:


1202

มีความแตกต่างเล็กน้อยในการสั่งซื้อ bytecode

2 * (i * i):

     iconst_2
     iload0
     iload0
     imul
     imul
     iadd

vs 2 * i * i:

     iconst_2
     iload0
     imul
     iload0
     imul
     iadd

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

ดังนั้นเราจึงจำเป็นที่จะขุดลึกลงไปในระดับที่ต่ำกว่า (JIT) 1

จำไว้ว่า JIT มีแนวโน้มที่จะคลี่ลูปเล็ก ๆ อย่างจริงจัง อันที่จริงเราสังเกต 16x unrolling สำหรับ2 * (i * i)กรณี:

030   B2: # B2 B3 <- B1 B2  Loop: B2-B2 inner main of N18 Freq: 1e+006
030     addl    R11, RBP    # int
033     movl    RBP, R13    # spill
036     addl    RBP, #14    # int
039     imull   RBP, RBP    # int
03c     movl    R9, R13 # spill
03f     addl    R9, #13 # int
043     imull   R9, R9  # int
047     sall    RBP, #1
049     sall    R9, #1
04c     movl    R8, R13 # spill
04f     addl    R8, #15 # int
053     movl    R10, R8 # spill
056     movdl   XMM1, R8    # spill
05b     imull   R10, R8 # int
05f     movl    R8, R13 # spill
062     addl    R8, #12 # int
066     imull   R8, R8  # int
06a     sall    R10, #1
06d     movl    [rsp + #32], R10    # spill
072     sall    R8, #1
075     movl    RBX, R13    # spill
078     addl    RBX, #11    # int
07b     imull   RBX, RBX    # int
07e     movl    RCX, R13    # spill
081     addl    RCX, #10    # int
084     imull   RCX, RCX    # int
087     sall    RBX, #1
089     sall    RCX, #1
08b     movl    RDX, R13    # spill
08e     addl    RDX, #8 # int
091     imull   RDX, RDX    # int
094     movl    RDI, R13    # spill
097     addl    RDI, #7 # int
09a     imull   RDI, RDI    # int
09d     sall    RDX, #1
09f     sall    RDI, #1
0a1     movl    RAX, R13    # spill
0a4     addl    RAX, #6 # int
0a7     imull   RAX, RAX    # int
0aa     movl    RSI, R13    # spill
0ad     addl    RSI, #4 # int
0b0     imull   RSI, RSI    # int
0b3     sall    RAX, #1
0b5     sall    RSI, #1
0b7     movl    R10, R13    # spill
0ba     addl    R10, #2 # int
0be     imull   R10, R10    # int
0c2     movl    R14, R13    # spill
0c5     incl    R14 # int
0c8     imull   R14, R14    # int
0cc     sall    R10, #1
0cf     sall    R14, #1
0d2     addl    R14, R11    # int
0d5     addl    R14, R10    # int
0d8     movl    R10, R13    # spill
0db     addl    R10, #3 # int
0df     imull   R10, R10    # int
0e3     movl    R11, R13    # spill
0e6     addl    R11, #5 # int
0ea     imull   R11, R11    # int
0ee     sall    R10, #1
0f1     addl    R10, R14    # int
0f4     addl    R10, RSI    # int
0f7     sall    R11, #1
0fa     addl    R11, R10    # int
0fd     addl    R11, RAX    # int
100     addl    R11, RDI    # int
103     addl    R11, RDX    # int
106     movl    R10, R13    # spill
109     addl    R10, #9 # int
10d     imull   R10, R10    # int
111     sall    R10, #1
114     addl    R10, R11    # int
117     addl    R10, RCX    # int
11a     addl    R10, RBX    # int
11d     addl    R10, R8 # int
120     addl    R9, R10 # int
123     addl    RBP, R9 # int
126     addl    RBP, [RSP + #32 (32-bit)]   # int
12a     addl    R13, #16    # int
12e     movl    R11, R13    # spill
131     imull   R11, R13    # int
135     sall    R11, #1
138     cmpl    R13, #999999985
13f     jl     B2   # loop end  P=1.000000 C=6554623.000000

เราเห็นว่ามี 1 ทะเบียนที่ "หก" ลงบนสแต็ก

และสำหรับ2 * i * iเวอร์ชั่น:

05a   B3: # B2 B4 <- B1 B2  Loop: B3-B2 inner main of N18 Freq: 1e+006
05a     addl    RBX, R11    # int
05d     movl    [rsp + #32], RBX    # spill
061     movl    R11, R8 # spill
064     addl    R11, #15    # int
068     movl    [rsp + #36], R11    # spill
06d     movl    R11, R8 # spill
070     addl    R11, #14    # int
074     movl    R10, R9 # spill
077     addl    R10, #16    # int
07b     movdl   XMM2, R10   # spill
080     movl    RCX, R9 # spill
083     addl    RCX, #14    # int
086     movdl   XMM1, RCX   # spill
08a     movl    R10, R9 # spill
08d     addl    R10, #12    # int
091     movdl   XMM4, R10   # spill
096     movl    RCX, R9 # spill
099     addl    RCX, #10    # int
09c     movdl   XMM6, RCX   # spill
0a0     movl    RBX, R9 # spill
0a3     addl    RBX, #8 # int
0a6     movl    RCX, R9 # spill
0a9     addl    RCX, #6 # int
0ac     movl    RDX, R9 # spill
0af     addl    RDX, #4 # int
0b2     addl    R9, #2  # int
0b6     movl    R10, R14    # spill
0b9     addl    R10, #22    # int
0bd     movdl   XMM3, R10   # spill
0c2     movl    RDI, R14    # spill
0c5     addl    RDI, #20    # int
0c8     movl    RAX, R14    # spill
0cb     addl    RAX, #32    # int
0ce     movl    RSI, R14    # spill
0d1     addl    RSI, #18    # int
0d4     movl    R13, R14    # spill
0d7     addl    R13, #24    # int
0db     movl    R10, R14    # spill
0de     addl    R10, #26    # int
0e2     movl    [rsp + #40], R10    # spill
0e7     movl    RBP, R14    # spill
0ea     addl    RBP, #28    # int
0ed     imull   RBP, R11    # int
0f1     addl    R14, #30    # int
0f5     imull   R14, [RSP + #36 (32-bit)]   # int
0fb     movl    R10, R8 # spill
0fe     addl    R10, #11    # int
102     movdl   R11, XMM3   # spill
107     imull   R11, R10    # int
10b     movl    [rsp + #44], R11    # spill
110     movl    R10, R8 # spill
113     addl    R10, #10    # int
117     imull   RDI, R10    # int
11b     movl    R11, R8 # spill
11e     addl    R11, #8 # int
122     movdl   R10, XMM2   # spill
127     imull   R10, R11    # int
12b     movl    [rsp + #48], R10    # spill
130     movl    R10, R8 # spill
133     addl    R10, #7 # int
137     movdl   R11, XMM1   # spill
13c     imull   R11, R10    # int
140     movl    [rsp + #52], R11    # spill
145     movl    R11, R8 # spill
148     addl    R11, #6 # int
14c     movdl   R10, XMM4   # spill
151     imull   R10, R11    # int
155     movl    [rsp + #56], R10    # spill
15a     movl    R10, R8 # spill
15d     addl    R10, #5 # int
161     movdl   R11, XMM6   # spill
166     imull   R11, R10    # int
16a     movl    [rsp + #60], R11    # spill
16f     movl    R11, R8 # spill
172     addl    R11, #4 # int
176     imull   RBX, R11    # int
17a     movl    R11, R8 # spill
17d     addl    R11, #3 # int
181     imull   RCX, R11    # int
185     movl    R10, R8 # spill
188     addl    R10, #2 # int
18c     imull   RDX, R10    # int
190     movl    R11, R8 # spill
193     incl    R11 # int
196     imull   R9, R11 # int
19a     addl    R9, [RSP + #32 (32-bit)]    # int
19f     addl    R9, RDX # int
1a2     addl    R9, RCX # int
1a5     addl    R9, RBX # int
1a8     addl    R9, [RSP + #60 (32-bit)]    # int
1ad     addl    R9, [RSP + #56 (32-bit)]    # int
1b2     addl    R9, [RSP + #52 (32-bit)]    # int
1b7     addl    R9, [RSP + #48 (32-bit)]    # int
1bc     movl    R10, R8 # spill
1bf     addl    R10, #9 # int
1c3     imull   R10, RSI    # int
1c7     addl    R10, R9 # int
1ca     addl    R10, RDI    # int
1cd     addl    R10, [RSP + #44 (32-bit)]   # int
1d2     movl    R11, R8 # spill
1d5     addl    R11, #12    # int
1d9     imull   R13, R11    # int
1dd     addl    R13, R10    # int
1e0     movl    R10, R8 # spill
1e3     addl    R10, #13    # int
1e7     imull   R10, [RSP + #40 (32-bit)]   # int
1ed     addl    R10, R13    # int
1f0     addl    RBP, R10    # int
1f3     addl    R14, RBP    # int
1f6     movl    R10, R8 # spill
1f9     addl    R10, #16    # int
1fd     cmpl    R10, #999999985
204     jl     B2   # loop end  P=1.000000 C=7419903.000000

ที่นี่เราสังเกต "การรั่วไหล" มากขึ้นและเข้าถึงสแต็[RSP + ...]กได้มากขึ้นเนื่องจากผลลัพธ์ระดับกลางที่จำเป็นต้องเก็บรักษาไว้

ดังนั้นคำตอบสำหรับคำถามนั้นง่าย: 2 * (i * i)เร็วกว่า2 * i * iเพราะ JIT สร้างรหัสแอสเซมบลีที่เหมาะสมที่สุดสำหรับกรณีแรก


แต่แน่นอนเป็นที่ชัดเจนว่าทั้งรุ่นแรกและรุ่นที่สองนั้นไม่ดีเลย การวนซ้ำจะได้ประโยชน์จาก vectorization จริง ๆ เนื่องจาก x86-64 CPU ใด ๆ มีการสนับสนุน SSE2 เป็นอย่างน้อย

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

ในความเป็นจริงแล้วซีพียู x86-64 รุ่นใหม่แบ่งคำแนะนำเพิ่มเติมเป็น micro-ops (µops) และด้วยคุณสมบัติต่างๆเช่นการเปลี่ยนชื่อรีจิสเตอร์แคชแคชและบัฟเฟอร์บัฟเฟอร์ ตามคู่มือการเพิ่มประสิทธิภาพของ Agner Fog :

การเพิ่มประสิทธิภาพในการทำงานเนื่องจากแคช canop นั้นค่อนข้างมากหากความยาวคำสั่งเฉลี่ยมากกว่า 4 ไบต์ วิธีการต่อไปนี้ของการปรับการใช้แคช µop ให้เหมาะสมอาจได้รับการพิจารณา:

  • ตรวจสอบให้แน่ใจว่าลูปวิกฤตมีขนาดเล็กพอที่จะใส่ลงในแคชแคชได้
  • จัดแนวรายการวนซ้ำที่สำคัญที่สุดและรายการฟังก์ชันให้เท่ากับ 32
  • หลีกเลี่ยงการวนซ้ำที่ไม่จำเป็น

  • คำแนะนำหลีกเลี่ยงการที่มีเวลาในการโหลดพิเศษ . .

เกี่ยวกับความเร็วในการโหลดแม้ความเร็ว L1D ที่เร็วที่สุดจะเสียค่าใช้จ่าย 4 รอบการลงทะเบียนพิเศษและ µop ดังนั้นใช่แม้การเข้าถึงหน่วยความจำเพียงไม่กี่ครั้งก็จะส่งผลเสียต่อประสิทธิภาพการทำงานในลูป

แต่กลับไปที่โอกาสการทำให้เป็นเวกเตอร์ - เพื่อดูว่ามันเร็วแค่ไหนเราสามารถรวบรวมแอปพลิเคชั่น C ที่คล้ายกันกับ GCCซึ่งเวกเตอร์ที่ตรงข้ามกับมัน (แสดง AVX2, SSE2 คล้ายกัน) 2 :

  vmovdqa ymm0, YMMWORD PTR .LC0[rip]
  vmovdqa ymm3, YMMWORD PTR .LC1[rip]
  xor eax, eax
  vpxor xmm2, xmm2, xmm2
.L2:
  vpmulld ymm1, ymm0, ymm0
  inc eax
  vpaddd ymm0, ymm0, ymm3
  vpslld ymm1, ymm1, 1
  vpaddd ymm2, ymm2, ymm1
  cmp eax, 125000000      ; 8 calculations per iteration
  jne .L2
  vmovdqa xmm0, xmm2
  vextracti128 xmm2, ymm2, 1
  vpaddd xmm2, xmm0, xmm2
  vpsrldq xmm0, xmm2, 8
  vpaddd xmm0, xmm2, xmm0
  vpsrldq xmm1, xmm0, 4
  vpaddd xmm0, xmm0, xmm1
  vmovd eax, xmm0
  vzeroupper

ด้วยเวลาทำงาน:

  • SSE: 0.24 วินาทีหรือเร็วกว่า 2 เท่า
  • AVX: 0.15 วินาทีหรือเร็วกว่า 3 เท่า
  • AVX2: 0.08 วินาทีหรือเร็วกว่า 5 เท่า

1 ในการรับ JIT สร้างเอาต์พุตแอสเซมบลีรับการดีบัก JVMและรันด้วย-XX:+PrintOptoAssembly

2 เวอร์ชั่น C ถูกคอมไพล์ด้วย-fwrapvแฟล็กซึ่งช่วยให้ GCC จัดการกับการโอเวอร์โฟลว์จำนวนเต็มที่ลงนามแล้วซึ่งเป็นส่วนเสริมของทั้งสอง


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

44
@Damon ทำไมพฤติกรรมที่ไม่ได้กำหนดจะเป็นปัญหาสำหรับเครื่องมือเพิ่มประสิทธิภาพ หากเครื่องมือเพิ่มประสิทธิภาพเห็นว่าโอเวอร์โฟลว์เมื่อพยายามคำนวณผลลัพธ์นั่นหมายความว่าสามารถเพิ่มประสิทธิภาพได้ตามต้องการเนื่องจากพฤติกรรมนั้นไม่ได้กำหนดไว้

13
@ Runemoro: หากเครื่องมือเพิ่มประสิทธิภาพพิสูจน์ว่าการเรียกฟังก์ชั่นนั้นจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดอย่างหลีกเลี่ยงไม่ได้ก็สามารถเลือกที่จะสันนิษฐานได้ว่าฟังก์ชั่นนี้จะไม่ถูกเรียกใช้ หรือปล่อยแค่retคำสั่งหรือปล่อยฉลากและไม่มีคำสั่ง ret เพื่อให้การดำเนินการผ่านไป ในความเป็นจริงแล้ว GCC ทำตัวเป็นแบบนี้บางครั้งเมื่อพบ UB ตัวอย่างเช่น: เหตุใด ret จึงหายไปพร้อมกับการเพิ่มประสิทธิภาพ . คุณต้องการรวบรวมรหัสที่มีรูปแบบที่แน่นอนเพื่อให้แน่ใจว่า asm นั้นมีเหตุผล
Peter Cordes

8
อาจเป็นเพียงคอขวดสำหรับปริมาณงานส่วนเกินเนื่องจากโค้ดที่ไม่มีประสิทธิภาพ มันไม่ได้เป็นแม้กระทั่งการใช้หน่วยงาน LEA เป็นช่องมองหา/mov add-immediateเช่น movl RBX, R9/ addl RBX, #8ควรเป็นleal ebx, [r9 + 8]1 uop เพื่อคัดลอกและเพิ่ม หรือจะทำอย่างไรleal ebx, [r9 + r9 + 16] ebx = 2*(r9+8)ใช่แล้วการคลี่ไปยังจุดที่หกนั้นเป็นใบ้และ codegen ไร้เดียงสาที่ไร้เดียงสาที่ไม่ได้ใช้ประโยชน์จากตัวตนจำนวนเต็มและคณิตศาสตร์จำนวนเต็มแบบเชื่อมโยง
Peter Cordes

7
Vectorization สำหรับการลดตามลำดับถูกปิดใช้งานใน C2 ( bugs.openjdk.java.net/browse/JDK-8078563 ) แต่ขณะนี้อยู่ระหว่างการพิจารณาการเปิดใช้งานอีกครั้ง ( bugs.openjdk.java.net/browse/JDK-8188313 )
pron

131

เมื่อการคูณคือ2 * (i * i)JVM สามารถแยกตัวคูณการคูณด้วย2จากลูปทำให้โค้ดที่เทียบเท่า แต่มีประสิทธิภาพมากกว่านี้:

int n = 0;
for (int i = 0; i < 1000000000; i++) {
    n += i * i;
}
n *= 2;

แต่เมื่อการคูณคือ(2 * i) * iJVM จะไม่ปรับให้เหมาะสมเนื่องจากการคูณด้วยค่าคงที่จะไม่ถูกต้องอีกต่อไปก่อนการเพิ่ม

นี่คือสาเหตุบางประการที่ฉันคิดว่าเป็นกรณีนี้:

  • การเพิ่มif (n == 0) n = 1คำสั่งที่จุดเริ่มต้นของลูปจะส่งผลให้ทั้งสองเวอร์ชันมีประสิทธิภาพเนื่องจากแฟคตอริ่งการคูณไม่รับประกันว่าผลลัพธ์จะเหมือนเดิมอีกต่อไป
  • รุ่นที่ได้รับการปรับปรุง (โดยแยกการคูณด้วย 2) นั้นเร็วเท่ากับ2 * (i * i)รุ่น

นี่คือรหัสทดสอบที่ฉันใช้ในการสรุปผลเหล่านี้:

public static void main(String[] args) {
    long fastVersion = 0;
    long slowVersion = 0;
    long optimizedVersion = 0;
    long modifiedFastVersion = 0;
    long modifiedSlowVersion = 0;

    for (int i = 0; i < 10; i++) {
        fastVersion += fastVersion();
        slowVersion += slowVersion();
        optimizedVersion += optimizedVersion();
        modifiedFastVersion += modifiedFastVersion();
        modifiedSlowVersion += modifiedSlowVersion();
    }

    System.out.println("Fast version: " + (double) fastVersion / 1000000000 + " s");
    System.out.println("Slow version: " + (double) slowVersion / 1000000000 + " s");
    System.out.println("Optimized version: " + (double) optimizedVersion / 1000000000 + " s");
    System.out.println("Modified fast version: " + (double) modifiedFastVersion / 1000000000 + " s");
    System.out.println("Modified slow version: " + (double) modifiedSlowVersion / 1000000000 + " s");
}

private static long fastVersion() {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        n += 2 * (i * i);
    }
    return System.nanoTime() - startTime;
}

private static long slowVersion() {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        n += 2 * i * i;
    }
    return System.nanoTime() - startTime;
}

private static long optimizedVersion() {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        n += i * i;
    }
    n *= 2;
    return System.nanoTime() - startTime;
}

private static long modifiedFastVersion() {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        if (n == 0) n = 1;
        n += 2 * (i * i);
    }
    return System.nanoTime() - startTime;
}

private static long modifiedSlowVersion() {
    long startTime = System.nanoTime();
    int n = 0;
    for (int i = 0; i < 1000000000; i++) {
        if (n == 0) n = 1;
        n += 2 * i * i;
    }
    return System.nanoTime() - startTime;
}

และนี่คือผลลัพธ์:

Fast version: 5.7274411 s
Slow version: 7.6190804 s
Optimized version: 5.1348007 s
Modified fast version: 7.1492705 s
Modified slow version: 7.2952668 s

3
ฉันคิดว่ารุ่นที่ดีที่สุดควรเป็นn *= 2000000000;
StefansArya

4
@StefansArya - ฉบับที่พิจารณากรณีที่มีข้อ จำกัด คือ 4 2*1*1 + 2*2*2 + 2*3*3และเรากำลังพยายามที่จะคำนวณ เห็นได้ชัดว่าการคำนวณ1*1 + 2*2 + 3*3และการคูณด้วย 2 นั้นถูกต้องในขณะที่การคูณด้วย 8 จะไม่เป็นเช่นนั้น
Martin Bonner สนับสนุน Monica

5
2(1²) + 2(2²) + 2(3²) = 2(1² + 2² + 3²)สมการทางคณิตศาสตร์เป็นเหมือนเพียงแค่นี้ นั่นง่ายมากและฉันลืมไปแล้วเพราะการวนซ้ำเพิ่มขึ้น
StefansArya

5
หากคุณพิมพ์ชุดประกอบโดยใช้ดีบัก jvm สิ่งนี้ดูเหมือนจะไม่ถูกต้อง คุณจะเห็นแซล ... พวง # 1 ซึ่งคูณด้วย 2 ในลูป น่าสนใจว่าเวอร์ชั่นที่ช้ากว่านั้นดูเหมือนจะไม่มีตัวคูณในลูป
Daniel Berlin

2
เหตุใดจึงเป็นปัจจัย JVM ออกจาก 2 2 * (i * i)แต่ไม่ได้มาจาก(2 * i) * i? ฉันคิดว่าพวกเขาเท่าเทียมกัน (นั่นอาจเป็นข้อสันนิษฐานที่ไม่ดีของฉัน) ถ้าเป็นเช่นนั้น JVM จะไม่ยอมรับการแสดงออกก่อนที่จะปรับให้เหมาะสมหรือไม่
RedSpikeyThing

41

รหัสไบต์: https://cs.nyu.edu/courses/fall00/V22.0201-001/jvm2.html โปรแกรมดูรหัสไบต์: https://github.com/Konloch/bytecode-viewer

ใน JDK ของฉัน (Windows 10 64 บิต 1.8.0_65-b17) ฉันสามารถทำซ้ำและอธิบาย:

public static void main(String[] args) {
    int repeat = 10;
    long A = 0;
    long B = 0;
    for (int i = 0; i < repeat; i++) {
        A += test();
        B += testB();
    }

    System.out.println(A / repeat + " ms");
    System.out.println(B / repeat + " ms");
}


private static long test() {
    int n = 0;
    for (int i = 0; i < 1000; i++) {
        n += multi(i);
    }
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000000; i++) {
        n += multi(i);
    }
    long ms = (System.currentTimeMillis() - startTime);
    System.out.println(ms + " ms A " + n);
    return ms;
}


private static long testB() {
    int n = 0;
    for (int i = 0; i < 1000; i++) {
        n += multiB(i);
    }
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1000000000; i++) {
        n += multiB(i);
    }
    long ms = (System.currentTimeMillis() - startTime);
    System.out.println(ms + " ms B " + n);
    return ms;
}

private static int multiB(int i) {
    return 2 * (i * i);
}

private static int multi(int i) {
    return 2 * i * i;
}

เอาท์พุท:

...
405 ms A 785527736
327 ms B 785527736
404 ms A 785527736
329 ms B 785527736
404 ms A 785527736
328 ms B 785527736
404 ms A 785527736
328 ms B 785527736
410 ms
333 ms

แล้วทำไมล่ะ รหัสไบต์คือ:

 private static multiB(int arg0) { // 2 * (i * i)
     <localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>

     L1 {
         iconst_2
         iload0
         iload0
         imul
         imul
         ireturn
     }
     L2 {
     }
 }

 private static multi(int arg0) { // 2 * i * i
     <localVar:index=0, name=i , desc=I, sig=null, start=L1, end=L2>

     L1 {
         iconst_2
         iload0
         imul
         iload0
         imul
         ireturn
     }
     L2 {
     }
 }

ความแตกต่าง: ด้วยวงเล็บ ( 2 * (i * i)):

  • กด const stack
  • กดโลคัลบนสแต็ก
  • กดโลคัลบนสแต็ก
  • คูณด้านบนของสแต็ก
  • คูณด้านบนของสแต็ก

ไม่มีวงเล็บ ( 2 * i * i):

  • กด const stack
  • กดโลคัลบนสแต็ก
  • คูณด้านบนของสแต็ก
  • กดโลคัลบนสแต็ก
  • คูณด้านบนของสแต็ก

การโหลดทั้งหมดบนสแต็กจากนั้นการทำงานกลับจะเร็วกว่าการสลับระหว่างการวางสแต็กและการดำเนินการกับมัน


แต่ทำไม push-push-multiply-multiply เร็วกว่า push-multiply-push-multiply?
m0skit0

35

Kasperdถามในความคิดเห็นของคำตอบที่ยอมรับ:

ตัวอย่าง Java และ C ใช้ชื่อรีจิสเตอร์ที่แตกต่างกันมาก ทั้งสองตัวอย่างใช้ AMD64 ISA หรือไม่

xor edx, edx
xor eax, eax
.L2:
mov ecx, edx
imul ecx, edx
add edx, 1
lea eax, [rax+rcx*2]
cmp edx, 1000000000
jne .L2

ฉันไม่มีชื่อเสียงพอที่จะตอบคำถามนี้ในความคิดเห็น แต่สิ่งเหล่านี้เป็น ISA เดียวกัน เป็นค่าที่ชี้ให้เห็นว่ารุ่น GCC ใช้ตรรกะจำนวนเต็ม 32 บิตและรุ่นที่รวบรวม JVM ใช้ตรรกะจำนวนเต็ม 64 บิตภายใน

R8 จะ R15 เป็นเพียง x86_64 ใหม่ลงทะเบียน EAX to EDX เป็นส่วนล่างของการลงทะเบียนวัตถุประสงค์ทั่วไปของ RAX to RDX ส่วนที่สำคัญในคำตอบก็คือเวอร์ชัน GCC จะไม่ถูกเปิดใช้งาน มันจะประมวลผลลูปหนึ่งรอบต่อหนึ่งลูปเครื่องจริง ในขณะที่รุ่น JVM มีลูป 16 รอบในลูปทางกายภาพเดียว (ขึ้นอยู่กับคำตอบของ rustyx ฉันไม่ได้ตีความแอสเซมบลีซ้ำ) นี่คือหนึ่งในเหตุผลที่มีการลงทะเบียนมากกว่านี้เนื่องจากร่างกายของลูปนั้นยาวกว่า 16 เท่า


2
gcc ที่แย่มากไม่ได้สังเกตว่ามันสามารถทำให้*2หลุดออกจากวงได้ แม้ว่าในกรณีนี้มันไม่ได้เป็นเรื่องชนะเพราะทำด้วยฟรีกับ LEA อินเทลซีพียูlea eax, [rax+rcx*2]มีแฝง 1c add eax,ecxเดียวกับ อย่างไรก็ตามบน CPU ของ AMD ดัชนีใด ๆ ที่ปรับขนาดจะเพิ่มเวลาแฝงของ LEA เป็น 2 รอบ ดังนั้นห่วงโซ่พึ่งพาแบบวนซ้ำจึงยืดได้ถึง 2 รอบกลายเป็นคอขวดบน Ryzen ( imul ecx,edxปริมาณงานคือ 1 ต่อนาฬิกาใน Ryzen และ Intel)
Peter Cordes

31

ในขณะที่ไม่เกี่ยวข้องโดยตรงกับสภาพแวดล้อมของคำถามเพียงเพื่อความอยากรู้อยากเห็นฉันทำแบบทดสอบเดียวกันใน. NET Core 2.1, x64, โหมดการเปิดตัว

นี่คือผลลัพธ์ที่น่าสนใจโดยยืนยันว่ามีสิ่งที่คล้ายกันเกิดขึ้น (ด้านอื่น ๆ ) ที่เกิดขึ้นในด้านมืดของแรง รหัส:

static void Main(string[] args)
{
    Stopwatch watch = new Stopwatch();

    Console.WriteLine("2 * (i * i)");

    for (int a = 0; a < 10; a++)
    {
        int n = 0;

        watch.Restart();

        for (int i = 0; i < 1000000000; i++)
        {
            n += 2 * (i * i);
        }

        watch.Stop();

        Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds} ms");
    }

    Console.WriteLine();
    Console.WriteLine("2 * i * i");

    for (int a = 0; a < 10; a++)
    {
        int n = 0;

        watch.Restart();

        for (int i = 0; i < 1000000000; i++)
        {
            n += 2 * i * i;
        }

        watch.Stop();

        Console.WriteLine($"result:{n}, {watch.ElapsedMilliseconds}ms");
    }
}

ผลลัพธ์:

2 * (i * i)

  • ผล: 119860736, 438 ms
  • ผล: 119860736, 433 ms
  • ผล: 119860736, 437 ms
  • ผลลัพธ์: 119860736, 435 ms
  • ผล: 119860736, 436 ms
  • ผลลัพธ์: 119860736, 435 ms
  • ผลลัพธ์: 119860736, 435 ms
  • ผลลัพธ์: 119860736, 439 ms
  • ผล: 119860736, 436 ms
  • ผล: 119860736, 437 ms

2 * i * i

  • ผลลัพธ์: 119860736, 417 ms
  • ผลลัพธ์: 119860736, 417 ms
  • ผลลัพธ์: 119860736, 417 ms
  • ผลลัพธ์: 119860736, 418 ms
  • ผลลัพธ์: 119860736, 418 ms
  • ผลลัพธ์: 119860736, 417 ms
  • ผลลัพธ์: 119860736, 418 ms
  • ผล: 119860736, 416 ms
  • ผลลัพธ์: 119860736, 417 ms
  • ผลลัพธ์: 119860736, 418 ms

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

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

1
มันเป็นลิงค์ไปยัง imgur ดังนั้นใช่มันไม่สำคัญว่าคุณจะเพิ่มลิงค์อย่างไร ฉันล้มเหลวที่จะเห็นสิ่งที่ยากมากเกี่ยวกับการคัดลอกการวางคอนโซลเอาต์พุต
Jared Smith

5
ยกเว้นวิธีนี้เป็นวิธีอื่น ๆ
leppie

2
@SamB ยังคงอยู่ในโดเมน imgur.com ซึ่งหมายความว่าจะสามารถอยู่รอดได้ตราบเท่าที่ imgur
p91paul

21

ฉันได้รับผลลัพธ์ที่คล้ายกัน:

2 * (i * i): 0.458765943 s, n=119860736
2 * i * i: 0.580255126 s, n=119860736

ฉันได้รับSAMEผลลัพธ์หากลูปทั้งคู่อยู่ในโปรแกรมเดียวกันหรือแต่ละไฟล์อยู่ในไฟล์. java / .class แยกต่างหากซึ่งถูกดำเนินการในการรันแยกกัน

ในที่สุดนี่คือjavap -c -v <.java>decompile ของแต่ละ:

     3: ldc           #3                  // String 2 * (i * i):
     5: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
     8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J
     8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J
    11: lstore_1
    12: iconst_0
    13: istore_3
    14: iconst_0
    15: istore        4
    17: iload         4
    19: ldc           #6                  // int 1000000000
    21: if_icmpge     40
    24: iload_3
    25: iconst_2
    26: iload         4
    28: iload         4
    30: imul
    31: imul
    32: iadd
    33: istore_3
    34: iinc          4, 1
    37: goto          17

เมื่อเทียบกับ

     3: ldc           #3                  // String 2 * i * i:
     5: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
     8: invokestatic  #5                  // Method java/lang/System.nanoTime:()J
    11: lstore_1
    12: iconst_0
    13: istore_3
    14: iconst_0
    15: istore        4
    17: iload         4
    19: ldc           #6                  // int 1000000000
    21: if_icmpge     40
    24: iload_3
    25: iconst_2
    26: iload         4
    28: imul
    29: iload         4
    31: imul
    32: iadd
    33: istore_3
    34: iinc          4, 1
    37: goto          17

FYI -

java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

1
คำตอบที่ดีกว่าและคุณสามารถโหวตให้ยกเลิกการลบได้ - stackoverflow.com/a/53452836/1746118 ... ข้อความด้านข้าง - ฉันไม่ใช่ผู้ลงคะแนนเลย
Naman

@nullpointer - ฉันเห็นด้วย ฉันจะลงคะแนนให้ยกเลิกการลบหากทำได้ ฉันยังต้องการที่จะ "double upvote" stefan เพื่อให้คำจำกัดความเชิงปริมาณของ "สำคัญ"
paulsm4

อันนั้นถูกลบตัวเองเพราะมันวัดสิ่งที่ผิด - ดูความคิดเห็นของผู้เขียนเกี่ยวกับคำถามข้างต้น
Krease

2
รับJRE แก้ปัญหา-XX:+PrintOptoAssemblyและทำงานด้วย หรือเพียงแค่ใช้ vtune หรือเหมือนกัน
rustyx

1
@ rustyx - หากปัญหาคือการใช้งาน JIT ... จากนั้น "รับรุ่นการแก้ปัญหา" ของ JRE ที่แตกต่างกันอย่างสมบูรณ์ไม่จำเป็นต้องช่วย อย่างไรก็ตาม: ดูเหมือนสิ่งที่คุณพบข้างต้นด้วยการถอดแยกชิ้นส่วน JIT ของคุณบน JRE ของคุณนอกจากนี้ยังอธิบายถึงพฤติกรรมใน JRE ของ OP และของฉัน และอธิบายว่าเพราะเหตุใด JRE คนอื่นจึงมีพฤติกรรม "แตกต่าง" +1: ขอบคุณสำหรับผลงานยอดเยี่ยม!
paulsm4

18

การสังเกตที่น่าสนใจโดยใช้Java 11และปิดการวนลูปด้วยตัวเลือก VM ต่อไปนี้:

-XX:LoopUnrollLimit=0

การวนซ้ำด้วย2 * (i * i)นิพจน์ทำให้โค้ดเนทีฟขนาดกะทัดรัด1 :

L0001: add    eax,r11d
       inc    r8d
       mov    r11d,r8d
       imul   r11d,r8d
       shl    r11d,1h
       cmp    r8d,r10d
       jl     L0001

เปรียบเทียบกับ2 * i * iเวอร์ชั่น:

L0001: add    eax,r11d
       mov    r11d,r8d
       shl    r11d,1h
       add    r11d,2h
       inc    r8d
       imul   r11d,r8d
       cmp    r8d,r10d
       jl     L0001

รุ่น Java:

java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

ผลการเปรียบเทียบ

Benchmark          (size)  Mode  Cnt    Score     Error  Units
LoopTest.fast  1000000000  avgt    5  694,868 ±  36,470  ms/op
LoopTest.slow  1000000000  avgt    5  769,840 ± 135,006  ms/op

รหัสที่มามาตรฐาน:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@Fork(1)
public class LoopTest {

    @Param("1000000000") private int size;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(LoopTest.class.getSimpleName())
            .jvmArgs("-XX:LoopUnrollLimit=0")
            .build();
        new Runner(opt).run();
    }

    @Benchmark
    public int slow() {
        int n = 0;
        for (int i = 0; i < size; i++)
            n += 2 * i * i;
        return n;
    }

    @Benchmark
    public int fast() {
        int n = 0;
        for (int i = 0; i < size; i++)
            n += 2 * (i * i);
        return n;
    }
}

1 - ตัวเลือก VM ที่ใช้: -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:LoopUnrollLimit=0


2
ว้าวนั่นคือสมองบางส่วน asm แทนที่จะเพิ่มขึ้นi ก่อนที่จะคัดลอกเพื่อคำนวณ2*iมันจะทำหลังจากนั้นจึงต้องการadd r11d,2คำสั่งพิเศษ (บวกมันพลาดadd same,sameตาแมวแทนที่จะเป็นshl1 (เพิ่มรันบนพอร์ตอื่น ๆ ) นอกจากนี้ยังพลาดช่องตาแมวของ LEA สำหรับx*2 + 2( lea r11d, [r8*2 + 2]) ถ้ามันต้องการทำสิ่งต่าง ๆ ตามลำดับด้วยเหตุผลการตั้งเวลาการเรียนการสอนที่บ้าคลั่งเราสามารถเห็นได้จาก รุ่นคลี่ที่หายออกไปจากหน่วยงาน LEA ถูกต้นทุนมันมากของ UOPs เช่นเดียวกับลูปทั้งสองที่นี่.
ปีเตอร์ Cordes

2
lea eax, [rax + r11 * 2]จะแทนที่ 2 คำแนะนำ (ในลูปทั้งคู่) หากคอมไพเลอร์ JIT มีเวลาในการค้นหาการปรับให้เหมาะสมนั้นในลูปที่รันเป็นเวลานาน ผู้แปลก่อนเวลาที่เหมาะสมจะพบ (ยกเว้นว่าจะปรับเฉพาะสำหรับ AMD ที่ดัชนี LEA ที่ปรับอัตราส่วนมีความหน่วงรอบ 2 รอบดังนั้นอาจไม่คุ้มค่า)
Peter Cordes

15

ฉันพยายาม JMH โดยใช้แม่แบบเริ่มต้น: ฉันยังเพิ่มรุ่นที่ดีที่สุดขึ้นอยู่กับคำอธิบายของ Runemoro

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//@BenchmarkMode({ Mode.All })
@BenchmarkMode(Mode.AverageTime)
public class MyBenchmark {
  @Param({ "100", "1000", "1000000000" })
  private int size;

  @Benchmark
  public int two_square_i() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += 2 * (i * i);
    }
    return n;
  }

  @Benchmark
  public int square_i_two() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += i * i;
    }
    return 2*n;
  }

  @Benchmark
  public int two_i_() {
    int n = 0;
    for (int i = 0; i < size; i++) {
      n += 2 * i * i;
    }
    return n;
  }
}

ผลลัพธ์อยู่ที่นี่:

Benchmark                           (size)  Mode  Samples          Score   Score error  Units
o.s.MyBenchmark.square_i_two           100  avgt       10         58,062         1,410  ns/op
o.s.MyBenchmark.square_i_two          1000  avgt       10        547,393        12,851  ns/op
o.s.MyBenchmark.square_i_two    1000000000  avgt       10  540343681,267  16795210,324  ns/op
o.s.MyBenchmark.two_i_                 100  avgt       10         87,491         2,004  ns/op
o.s.MyBenchmark.two_i_                1000  avgt       10       1015,388        30,313  ns/op
o.s.MyBenchmark.two_i_          1000000000  avgt       10  967100076,600  24929570,556  ns/op
o.s.MyBenchmark.two_square_i           100  avgt       10         70,715         2,107  ns/op
o.s.MyBenchmark.two_square_i          1000  avgt       10        686,977        24,613  ns/op
o.s.MyBenchmark.two_square_i    1000000000  avgt       10  652736811,450  27015580,488  ns/op

บนพีซีของฉัน ( Core i7 860 - ไม่ได้ทำอะไรนอกจากอ่านบนสมาร์ทโฟนของฉัน):

  • n += i*iจากนั้นn*2เป็นครั้งแรก
  • 2 * (i * i) เป็นครั้งที่สอง

JVM เห็นได้ชัดว่าไม่ได้ปรับวิธีการเดียวกันกับที่มนุษย์ทำ (ตามคำตอบของ Runemoro)

ตอนนี้การอ่าน bytecode: javap -c -v ./target/classes/org/sample/MyBenchmark.class

  • ความแตกต่างระหว่าง 2 * (i * i) (ซ้าย) และ 2 * i * i (ขวา) ที่นี่: https://www.diffchecker.com/cvSFppWI
  • ความแตกต่างระหว่าง 2 * (i * i) และรุ่นที่ปรับปรุงแล้วที่นี่: https://www.diffchecker.com/I1XFu5dP

ฉันไม่ใช่ผู้เชี่ยวชาญของ bytecode แต่iload_2ก่อนหน้านี้เราimul: นั่นอาจเป็นที่ที่คุณได้รับความแตกต่าง: ฉันสามารถสมมติว่า JVM เพิ่มประสิทธิภาพการอ่านiสองครั้ง ( iอยู่ที่นี่แล้วและไม่จำเป็นต้องโหลดอีกครั้ง) ในขณะที่2*i*iมันสามารถ ' เสื้อ


4
bytecode ของ AFAICT นั้นไม่เกี่ยวข้องกับประสิทธิภาพค่อนข้างมากและฉันจะไม่พยายามประเมินว่าอะไรจะเร็วกว่านี้ มันเป็นเพียงซอร์สโค้ดสำหรับคอมไพเลอร์ JIT ... แน่นอนว่าการรักษาบรรทัดซอร์สโค้ดที่มีความหมายซึ่งรักษาความหมายไว้นั้นจะเปลี่ยนโค้ดผลลัพธ์และประสิทธิภาพ แต่ก็ไม่สามารถคาดเดาได้ทั้งหมด
maaartinus

13

ภาคผนวกเพิ่มเติม ฉันทำซ้ำการทดลองโดยใช้ Java 8 JVM ล่าสุดจาก IBM:

java version "1.8.0_191"
Java(TM) 2 Runtime Environment, Standard Edition (IBM build 1.8.0_191-b12 26_Oct_2018_18_45 Mac OS X x64(SR5 FP25))
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

และสิ่งนี้แสดงผลลัพธ์ที่คล้ายกันมาก:

0.374653912 s
n = 119860736
0.447778698 s
n = 119860736

(ผลลัพธ์ที่สองใช้ 2 * i * i)

น่าสนใจพอเมื่อทำงานบนเครื่องเดียวกัน แต่ใช้ Oracle Java:

Java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

ผลลัพธ์โดยเฉลี่ยช้าลงเล็กน้อย:

0.414331815 s
n = 119860736
0.491430656 s
n = 119860736

เรื่องสั้น ๆ สั้น ๆ : แม้แต่ HotSpot ก็มีความสำคัญเพียงเล็กน้อยเท่านั้นเนื่องจากความแตกต่างเล็กน้อยในการดำเนินการของ JIT อาจมีผลกระทบที่น่าทึ่ง


5

วิธีการเพิ่มสองวิธีสร้างโค้ดไบต์ที่แตกต่างกันเล็กน้อย:

  17: iconst_2
  18: iload         4
  20: iload         4
  22: imul
  23: imul
  24: iadd

สำหรับ2 * (i * i)vs:

  17: iconst_2
  18: iload         4
  20: imul
  21: iload         4
  23: imul
  24: iadd

2 * i * iสำหรับ

และเมื่อใช้มาตรฐานJMHเช่นนี้:

@Warmup(iterations = 5, batchSize = 1)
@Measurement(iterations = 5, batchSize = 1)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class MyBenchmark {

    @Benchmark
    public int noBrackets() {
        int n = 0;
        for (int i = 0; i < 1000000000; i++) {
            n += 2 * i * i;
        }
        return n;
    }

    @Benchmark
    public int brackets() {
        int n = 0;
        for (int i = 0; i < 1000000000; i++) {
            n += 2 * (i * i);
        }
        return n;
    }

}

ความแตกต่างชัดเจน:

# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: <none>

Benchmark                      (n)  Mode  Cnt    Score    Error  Units
MyBenchmark.brackets    1000000000  avgt    5  380.889 ± 58.011  ms/op
MyBenchmark.noBrackets  1000000000  avgt    5  512.464 ± 11.098  ms/op

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

ทำงานอีกครั้งกับ Graal:

# JMH version: 1.21
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM options: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler

Benchmark                      (n)  Mode  Cnt    Score    Error  Units
MyBenchmark.brackets    1000000000  avgt    5  335.100 ± 23.085  ms/op
MyBenchmark.noBrackets  1000000000  avgt    5  331.163 ± 50.670  ms/op

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

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

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