ทำไมถ้า (variable1% variable2 == 0) ไม่มีประสิทธิภาพ


179

ฉันยังใหม่กับ java และใช้รหัสบางคืนเมื่อคืนและนี่รบกวนฉันจริงๆ ฉันกำลังสร้างโปรแกรมง่าย ๆ เพื่อแสดงเอาต์พุต X ทุกอันในลูป for และฉันสังเกตเห็นว่าประสิทธิภาพลดลงอย่างมากเมื่อฉันใช้โมดูลัสเป็นvariable % variablevs variable % 5000หรือ whatnot มีคนอธิบายให้ฉันฟังได้ไหมว่าเพราะอะไรและสาเหตุคืออะไร ดังนั้นฉันจะดีกว่า ...

นี่คือรหัส "ประสิทธิภาพ" (ขออภัยถ้าฉันได้รับไวยากรณ์เล็กน้อยผิดฉันไม่ได้อยู่ในคอมพิวเตอร์ด้วยรหัสตอนนี้)

long startNum = 0;
long stopNum = 1000000000L;

for (long i = startNum; i <= stopNum; i++){
    if (i % 50000 == 0) {
        System.out.println(i);
    }
}

นี่คือ "รหัสไม่มีประสิทธิภาพ"

long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;

for (long i = startNum; i <= stopNum; i++){
    if (i % progressCheck == 0) {
        System.out.println(i);
    }
}

โปรดทราบว่าฉันมีตัวแปรวันที่เพื่อวัดความแตกต่างและเมื่อมันนานพอตัวแรกจะใช้เวลา 50 มิลลิวินาทีในขณะที่อีกอันใช้เวลา 12 วินาทีหรืออย่างนั้น คุณอาจจะต้องเพิ่มstopNumหรือลดระดับprogressCheckหากพีซีของคุณมีประสิทธิภาพมากกว่าของฉันหรืออะไรก็ตาม

ฉันมองหาคำถามนี้ในเว็บ แต่ฉันไม่สามารถหาคำตอบได้บางทีฉันอาจจะไม่ได้ถามก็ได้

แก้ไข: ฉันไม่ได้คาดหวังว่าคำถามของฉันจะได้รับความนิยมฉันชื่นชมคำตอบทั้งหมด ฉันทำการวัดประสิทธิภาพในแต่ละครึ่งของเวลาและรหัสที่ไม่มีประสิทธิภาพใช้เวลานานกว่า 1/4 ของวินาทีกับ 10 วินาทีหรือให้ ได้รับพวกเขากำลังใช้ println แต่พวกเขาทั้งสองทำในปริมาณที่เท่ากันดังนั้นฉันจะไม่คิดว่ามันจะเอียงมากโดยเฉพาะอย่างยิ่งเนื่องจากความแตกต่างสามารถทำซ้ำได้ สำหรับคำตอบเนื่องจากฉันยังใหม่กับ Java ฉันจะให้คะแนนตัดสินใจตอนนี้คำตอบที่ดีที่สุด ฉันจะพยายามเลือกวันพุธ

EDIT2: ฉันจะทำการทดสอบอื่นคืนนี้แทนโมดูลัสมันแค่เพิ่มตัวแปรและเมื่อมาถึง progressCheck มันจะดำเนินการอย่างใดอย่างหนึ่งแล้วรีเซ็ตตัวแปรเป็น 0 สำหรับตัวเลือกที่ 3

EDIT3.5:

ฉันใช้รหัสนี้และด้านล่างฉันจะแสดงผลลัพธ์ของฉัน .. ขอบคุณทุกท่านสำหรับความช่วยเหลือที่ยอดเยี่ยม! ฉันยังลองเปรียบเทียบค่าสั้น ๆ ของความยาวเป็น 0 ดังนั้นเช็คใหม่ของฉันจะเกิดขึ้นตลอดเวลา "65536" ครั้งทำให้เท่ากันซ้ำ

public class Main {


    public static void main(String[] args) {

        long startNum = 0;
        long stopNum = 1000000000L;
        long progressCheck = 65536;
        final long finalProgressCheck = 50000;
        long date;

        // using a fixed value
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if (i % 65536 == 0) {
                System.out.println(i);
            }
        }
        long final1 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        //using a variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                System.out.println(i);
            }
        }
        long final2 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();

        // using a final declared variable
        for (long i = startNum; i <= stopNum; i++) {
            if (i % finalProgressCheck == 0) {
                System.out.println(i);
            }
        }
        long final3 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        // using increments to determine progressCheck
        int increment = 0;
        for (long i = startNum; i <= stopNum; i++) {
            if (increment == 65536) {
                System.out.println(i);
                increment = 0;
            }
            increment++;

        }

        //using a short conversion
        long final4 = System.currentTimeMillis() - date;
        date = System.currentTimeMillis();
        for (long i = startNum; i <= stopNum; i++) {
            if ((short)i == 0) {
                System.out.println(i);
            }
        }
        long final5 = System.currentTimeMillis() - date;

                System.out.println(
                "\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
    }
}

ผล:

  • fixed = 874 ms (ปกติประมาณ 1,000 มิลลิวินาที แต่เร็วขึ้นเนื่องจากมันเป็นกำลัง 2)
  • ตัวแปร = 8590 ms
  • ตัวแปรสุดท้าย = 1944 ms (คือ ~ 1000ms เมื่อใช้ 50000)
  • increment = 1904 ms
  • การแปลงสั้น = 679 ms

ไม่น่าแปลกใจที่เพียงพอเนื่องจากขาดการแบ่งแยกการแปลงสั้นจึงเร็วกว่าวิธีที่ "เร็ว" 23% นี่เป็นเรื่องที่น่าสนใจ หากคุณต้องการแสดงหรือเปรียบเทียบทุก ๆ 256 ครั้ง (หรือประมาณนั้น) คุณสามารถทำได้และใช้

if ((byte)integer == 0) {'Perform progress check code here'}

หมายเหตุสุดท้ายที่น่าสนใจโดยใช้โมดูลัสของ "ตัวแปรที่ประกาศครั้งสุดท้าย" กับ 65536 (ไม่ใช่ตัวเลขที่สวย) คือความเร็วครึ่งหนึ่งของ (ช้ากว่า) กว่าค่าคงที่ ที่ไหนก่อนที่มันจะถูกเปรียบเทียบใกล้กับความเร็วเดียวกัน


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


24
หารด้วยคงที่สามารถแปลงได้อย่างง่ายดายเพื่อการคูณโดยผกผัน หารด้วยตัวแปรไม่สามารถ และส่วน 32 บิตจะเร็วกว่าส่วนแบบ 64 บิตบนสถาปัตยกรรม x86
phuclv

2
@phuclv note การแบ่ง 32- บิตไม่ใช่ปัญหาที่นี่เป็นการดำเนินการที่เหลือ 64- บิตในทั้งสองกรณี
user85421

4
@RobertCotterman หากคุณประกาศตัวแปรเป็นครั้งสุดท้ายคอมไพเลอร์จะสร้างไบต์เดียวกันกับการใช้ค่าคงที่ (eclipse / Java 11) ((แม้จะใช้ช่องหน่วยความจำอีกหนึ่งช่องสำหรับตัวแปร))
user85421

คำตอบ:


139

คุณกำลังวัดOSR (บนกองทดแทน)ต้นขั้ว

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

ส่วนท้าย OSR นั้นไม่ได้รับการปรับให้เหมาะสมเหมือนกับวิธีปกติเพราะต้องการโครงร่างเฟรมที่เข้ากันได้กับเฟรมที่ตีความ ฉันพบนี้มาแล้วในคำตอบต่อไปนี้: 1 , 2 , 3

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

โดยเฉพาะอย่างยิ่งนี้ JIT หมายถึงไม่ได้แทนจำนวนเต็มส่วนกับการคูณ (ดูทำไม GCC ใช้การคูณด้วยจำนวนแปลก ๆ ในการใช้การหารจำนวนเต็มสำหรับเคล็ดลับ asm จากคอมไพเลอร์ล่วงหน้าเวลาเมื่อค่าเป็นค่าคงที่คอมไพล์เวลาหลังจากการอินไลน์ / การกระจายคงที่หากเปิดใช้งานการปรับเหล่านั้น . จำนวนเต็มที่ถูกต้องใน%นิพจน์จะได้รับการปรับให้เหมาะสมgcc -O0เช่นเดียวกับที่นี่ซึ่ง JITer ได้รับการปรับให้เหมาะสมแม้ใน Stub OSR)

อย่างไรก็ตามหากคุณใช้วิธีการเดียวกันหลาย ๆ ครั้งการทำงานครั้งที่สองและครั้งต่อไปจะเรียกใช้รหัสปกติ (ไม่ใช่ OSR) ซึ่งได้รับการปรับให้เหมาะสมที่สุด นี่คือเกณฑ์มาตรฐานเพื่อพิสูจน์ทฤษฎี (การเปรียบเทียบโดยใช้ JMH ):

@State(Scope.Benchmark)
public class Div {

    @Benchmark
    public void divConst(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % 50000 == 0) {
                blackhole.consume(i);
            }
        }
    }

    @Benchmark
    public void divVar(Blackhole blackhole) {
        long startNum = 0;
        long stopNum = 100000000L;
        long progressCheck = 50000;

        for (long i = startNum; i <= stopNum; i++) {
            if (i % progressCheck == 0) {
                blackhole.consume(i);
            }
        }
    }
}

และผลลัพธ์:

# Benchmark: bench.Div.divConst

# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 126,967 ms/op
# Warmup Iteration   2: 105,660 ms/op
# Warmup Iteration   3: 106,205 ms/op
Iteration   1: 105,620 ms/op
Iteration   2: 105,789 ms/op
Iteration   3: 105,915 ms/op
Iteration   4: 105,629 ms/op
Iteration   5: 105,632 ms/op


# Benchmark: bench.Div.divVar

# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration   1: 844,708 ms/op          <-- much slower!
# Warmup Iteration   2: 105,893 ms/op          <-- as fast as divConst
# Warmup Iteration   3: 105,601 ms/op
Iteration   1: 105,570 ms/op
Iteration   2: 105,475 ms/op
Iteration   3: 105,702 ms/op
Iteration   4: 105,535 ms/op
Iteration   5: 105,766 ms/op

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


5
ฉันลังเลที่จะลงคะแนนในเรื่องนี้ ในอีกด้านหนึ่งดูเหมือนว่าวิธีที่ซับซ้อนในการพูดว่า "คุณทำให้มาตรฐานของคุณสับสนอ่านอะไรเกี่ยวกับ JIT" ในทางกลับกันฉันสงสัยว่าทำไมคุณถึงแน่ใจว่า OSR เป็นประเด็นหลักที่เกี่ยวข้องที่นี่ ฉันหมายถึงการทำเกณฑ์มาตรฐาน (micro) ที่เกี่ยวข้องกับSystem.out.printlnเกือบจะจำเป็นต้องสร้างผลลัพธ์ขยะและความจริงที่ว่าทั้งสองรุ่นเร็วพอ ๆ กันไม่ต้องทำอะไรกับ OSR โดยเฉพาะอย่างยิ่งเท่าที่ฉันสามารถบอก ..
Marco13

2
(ฉันอยากรู้อยากเห็นและต้องการที่จะเข้าใจสิ่งนี้ฉันหวังว่าความคิดเห็นจะไม่ถูกรบกวนอาจลบได้ในภายหลัง แต่:) ลิงก์1ค่อนข้างน่าสงสัย - การวนซ้ำที่ว่างเปล่าสามารถปรับให้เหมาะสมได้อย่างสมบูรณ์ ประการที่สองคล้ายกับอันนั้นมากกว่า แต่อีกครั้งก็ไม่ชัดเจนว่าทำไมคุณบรรยายความแตกต่างในการ OSR เฉพาะ ฉันแค่จะบอกว่า: ในบางจุดวิธีการคือ JITed และกลายเป็นเร็วขึ้น เพื่อความเข้าใจของฉัน OSR ทำให้การใช้งานรหัสขั้นสุดท้ายที่ได้รับการปรับปรุงให้ดีที่สุด (โดยประมาณ) คือ ~ "ถูกเลื่อนไปสู่การเพิ่มประสิทธิภาพครั้งต่อไป" (ต่อ ... )
Marco13

1
(ต่อ :) ไม่ว่าคุณจะวิเคราะห์บันทึกฮอตสปอตโดยเฉพาะคุณไม่สามารถบอกได้ว่าความแตกต่างนั้นเกิดจากการเปรียบเทียบรหัส JITed และไม่ยกเลิก JITed หรือเปรียบเทียบ JITed และ OSR-stub-code และแน่นอนคุณไม่สามารถพูดได้ว่าแน่นอนเมื่อคำถามไม่ได้มีรหัสจริงหรือมาตรฐาน JMH ที่สมบูรณ์ ดังนั้นการโต้แย้งว่าความแตกต่างนั้นเกิดจากเสียง OSR สำหรับฉันเฉพาะเจาะจงอย่างไม่เหมาะสม (และ "ไม่ยุติธรรม") เมื่อเทียบกับการพูดว่ามันเกิดจาก JIT โดยทั่วไป (ไม่มีความผิด - ฉันแค่สงสัยว่า ... )
Marco13

4
@ Marco13 มีวิธีแก้ปัญหาแบบง่าย: หากไม่มีกิจกรรมของ JIT %การดำเนินการแต่ละอย่างจะมีน้ำหนักเท่ากันเนื่องจากการดำเนินการที่ได้รับการปรับให้เหมาะสมนั้นเป็นไปได้เพียงอย่างเดียวถ้าเครื่องมือเพิ่มประสิทธิภาพทำงานได้จริง ดังนั้นความจริงที่ว่าตัวแปรลูปหนึ่งนั้นเร็วกว่าอีกตัวแปรหนึ่งที่พิสูจน์การมีอยู่ของออพติไมซ์และต่อไปพิสูจน์ได้ว่ามันล้มเหลวในการปรับลูปอันใดอันหนึ่งให้อยู่ในระดับเดียวกันกับอีกอันหนึ่ง (ภายในวิธีเดียวกัน!) เมื่อคำตอบนี้พิสูจน์ความสามารถในการปรับให้เหมาะสมทั้งสองลูปในระดับเดียวกันจะต้องมีสิ่งที่ขัดขวางการเพิ่มประสิทธิภาพ และนั่นก็คือ OSR ใน 99.9% ของทุกกรณี
Holger

4
@ Marco13 นั่นคือ "การศึกษาที่คาดเดา" ตามความรู้ของ HotSpot Runtime และประสบการณ์ในการวิเคราะห์ปัญหาที่คล้ายกันมาก่อน การวนซ้ำแบบยาวนั้นแทบจะไม่สามารถรวบรวมในวิธีอื่นที่ไม่ใช่ OSR โดยเฉพาะอย่างยิ่งในการวัดด้วยมือที่เรียบง่าย ตอนนี้เมื่อ OP -XX:+PrintCompilation -XX:+TraceNMethodInstallsได้โพสต์รหัสสมบูรณ์ผมสามารถยืนยันเหตุผลอีกครั้งโดยใช้รหัสที่มี
apangin

42

ในการติดตามความคิดเห็น@phuclv ฉันตรวจสอบรหัสที่สร้างโดย JIT 1ผลลัพธ์มีดังนี้:

สำหรับvariable % 5000(หารด้วยค่าคงที่):

mov     rax,29f16b11c6d1e109h
imul    rbx
mov     r10,rbx
sar     r10,3fh
sar     rdx,0dh
sub     rdx,r10
imul    r10,rdx,0c350h    ; <-- imul
mov     r11,rbx
sub     r11,r10
test    r11,r11
jne     1d707ad14a0h

สำหรับvariable % variable:

mov     rax,r14
mov     rdx,8000000000000000h
cmp     rax,rdx
jne     22ccce218edh
xor     edx,edx
cmp     rbx,0ffffffffffffffffh
je      22ccce218f2h
cqo
idiv    rax,rbx           ; <-- idiv
test    rdx,rdx
jne     22ccce218c0h

เนื่องจากการหารใช้เวลานานกว่าการคูณเสมอข้อมูลโค้ดสุดท้ายจึงมีประสิทธิภาพน้อยกว่า

รุ่น 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)

1 - ตัวเลือก VM ที่ใช้: -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main


14
หากต้องการให้ลำดับความสำคัญเป็น "ช้าลง" สำหรับ x86_64: imulคือ 3 รอบidivคือระหว่าง 30 และ 90 รอบ ดังนั้นการหารจำนวนเต็มอยู่ระหว่าง 10x และ 30x ช้ากว่าการคูณจำนวนเต็ม
Matthieu M.

2
คุณช่วยอธิบายสิ่งเหล่านี้ได้ทั้งหมดสำหรับผู้อ่านที่สนใจ แต่ไม่พูดแอสเซมเบลอร์?
Nico Haase

7
@NicoHaase สองบรรทัดที่ถูกคอมเม้นท์เป็นบรรทัดที่สำคัญเท่านั้น ในส่วนแรกรหัสกำลังทำการคูณจำนวนเต็มในขณะที่ส่วนที่สองรหัสกำลังทำการหารจำนวนเต็ม หากคุณคิดว่าจะทำการคูณและหารด้วยมือเมื่อคุณคูณคุณมักจะคูณหลายครั้งเล็ก ๆ แล้วเพิ่มชุดใหญ่หนึ่งชุด แต่การหารคือส่วนเล็ก ๆ การคูณเล็กการลบและทำซ้ำ การหารช้าเพราะคุณทำทวีคูณเป็นจำนวนมาก
MBraedley

4
@ MBraedley ขอบคุณสำหรับการป้อนข้อมูลของคุณ แต่ควรเพิ่มคำอธิบายดังกล่าวลงในคำตอบและไม่ถูกซ่อนอยู่ในส่วนความคิดเห็น
Nico Haase

6
@ MBraedley: ยิ่งไปกว่านั้นการคูณใน CPU ที่ทันสมัยนั้นรวดเร็วเพราะผลิตภัณฑ์บางส่วนนั้นมีความเป็นอิสระและสามารถคำนวณแยกกันได้ในขณะที่แต่ละขั้นตอนของการหารขึ้นอยู่กับขั้นตอนก่อนหน้า
supercat

26

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

long progressCheck = 50000;

long counter = progressCheck;

for (long i = startNum; i <= stopNum; i++){
    if (--counter == 0) {
        System.out.println(i);
        counter = progressCheck;
    }
}

(เนื่องจากความพยายามในการ optmiziation เล็กน้อยเราใช้ pre-decrement down-counter ที่นี่เพราะในสถาปัตยกรรมหลาย ๆ ตัวเปรียบเทียบกับ0ทันทีหลังจากการดำเนินการทางคณิตศาสตร์มีค่าใช้จ่าย 0 คำแนะนำ / รอบการทำงานของ CPU อย่างแน่นอนเพราะค่าสถานะ ALU ถูกตั้งค่าไว้อย่างเหมาะสมแล้ว อย่างไรก็ตามคอมไพเลอร์จะทำการเพิ่มประสิทธิภาพนั้นโดยอัตโนมัติแม้ว่าคุณจะเขียนif (counter++ == 50000) { ... counter = 0; })

โปรดสังเกตว่าบ่อยครั้งที่คุณไม่ต้องการ / ต้องการโมดูลัสเพราะคุณรู้ว่าตัวนับลูปของคุณ ( i) หรืออะไรก็ตามที่เพิ่มขึ้นเพียง 1 และคุณไม่สนใจส่วนที่เหลือจริงโมดูลัสจะให้คุณเห็น หากตัวนับที่เพิ่มขึ้นหนึ่งตัวกระทบค่าบางอย่าง

อีก 'เคล็ดลับ' คือการใช้อำนาจของสองค่า / ข้อ จำกัด progressCheck = 1024;เช่น โมดูลัสอำนาจของทั้งสองสามารถคำนวณได้อย่างรวดเร็วผ่านทางค่าที่เหมาะสมคือand if ( (i & (1024-1)) == 0 ) {...}สิ่งนี้ควรจะค่อนข้างเร็วด้วยและในบางสถาปัตยกรรมอาจเหนือกว่าชัดเจนcounterกว่า


3
คอมไพเลอร์สมาร์ทจะกลับมาวนซ้ำที่นี่ หรือคุณสามารถทำได้ในแหล่งที่มา if()ร่างกายจะกลายเป็นร่างกายนอกวงและสิ่งนอกif()จะกลายเป็นห่วงร่างกายภายในที่วิ่งmin(progressCheck, stopNum-i)ซ้ำ ดังนั้นเมื่อเริ่มต้นและทุกครั้งที่counterถึง 0 คุณต้องlong next_stop = i + min(progressCheck, stopNum-i);ตั้งค่าการfor(; i< next_stop; i++) {}วนซ้ำ ในกรณีนี้ลูปด้านในจะว่างเปล่าและหวังว่าจะปรับให้เหมาะสมทั้งหมดคุณสามารถทำสิ่งนั้นในแหล่งที่มาและทำให้ JITer ง่ายขึ้นลดลูปของคุณเป็น i + = 50k
ปีเตอร์

2
แต่ใช่โดยทั่วไปตัวนับลงเป็นเทคนิคที่มีประสิทธิภาพที่ดีสำหรับสิ่งประเภท fizzbuzz / progresscheck
ปีเตอร์

ฉันเพิ่มคำถามของฉันและทำการเพิ่มขึ้น--counterอย่างรวดเร็วเท่ากับรุ่นที่เพิ่มขึ้นของฉัน แต่โค้ดน้อยลงนอกจากนี้มันคือ 1 ที่ต่ำกว่าที่ควรจะเป็นฉันอยากรู้อยากเห็นถ้าcounter--คุณต้องการจำนวนที่แน่นอนที่คุณต้องการ ไม่ใช่ว่ามันจะแตกต่างกันมาก
Robert Cotterman

@PeterCordes คอมไพเลอร์สมาร์ทจะพิมพ์ตัวเลขโดยไม่วนซ้ำเลย (ฉันคิดว่าบางมาตรฐานเล็กน้อยเล็กน้อยเริ่มล้มเหลวด้วยวิธีนี้อาจ 10 ปีที่แล้ว)
ปีเตอร์ - Reinstate Monica

2
@RobertCotterman ใช่--counterถูกปิดโดยหนึ่ง counter--จะให้progressCheckจำนวนการวนซ้ำแน่นอน (หรือคุณสามารถกำหนดให้progressCheck = 50001;แน่นอน)
JimmyB

4

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

for (long i = startNum; i <= stopNum; i++) {
    if (i % progressCheck == 0) {
        System.out.println(i)
    }
}

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

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

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

for (long i = startNum; i <= stopNum; i++) {
    if (i % 50000== 0) {
        System.out.println(i)
    }
}

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


1
มีความแตกต่างอย่างมากแม้ว่าฉันจะทำการดำเนินการเป็นล้านล้านครั้งดังนั้นการดำเนินงานมากกว่า 1 ล้านล้านครั้งก็ประหยัดเวลา 89% ในการทำรหัส "ประสิทธิภาพ" ใจคุณถ้าคุณทำเพียงไม่กี่พันครั้งกำลังพูดถึงความแตกต่างเล็ก ๆ น้อย ๆ มันอาจไม่ใช่เรื่องใหญ่ ฉันหมายถึงการปฏิบัติการมากกว่า 1,000 ครั้งมันจะช่วยให้คุณประหยัด 1 ล้านใน 7 วินาที
Robert Cotterman

1
@Bishal Dubey "จะไม่มีความแตกต่างกันในเวลาประมวลผลของโค้ดทั้งสอง" คุณอ่านคำถามหรือไม่
สิทธิ์ฟอสเตอร์เมื่อ

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