ทำไม (a * b! = 0) เร็วกว่า (a! = 0 && b! = 0) ใน Java


412

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

ฉันสามารถประเมินด้วย

if (a != 0 && b != 0) { /* Some code */ }

หรืออีกวิธีหนึ่ง

if (a*b != 0) { /* Some code */ }

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

long time;
final int len = 50000000;
int arbitrary = 0;
int[][] nums = new int[2][len];

for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) {
    for(int i = 0 ; i < 2 ; i++) {
        for(int j = 0 ; j < len ; j++) {
            double random = Math.random();

            if(random < fraction) nums[i][j] = 0;
            else nums[i][j] = (int) (random*15 + 1);
        }
    }

    time = System.currentTimeMillis();

    for(int i = 0 ; i < len ; i++) {
        if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++;
    }
    System.out.println(System.currentTimeMillis() - time);
}

และผลลัพธ์แสดงให้เห็นว่าหากคุณคาดหวังว่า "a" หรือ "b" จะเท่ากับ 0 มากกว่า ~ 3% ของเวลาa*b != 0จะเร็วกว่าa!=0 && b!=0:

กราฟแบบกราฟิกของผลลัพธ์ของ AND b ที่ไม่ใช่ศูนย์

ฉันอยากรู้ว่าทำไม ใครช่วยแสงบ้างไหม มันเป็นคอมไพเลอร์หรือมันเป็นระดับฮาร์ดแวร์?

แก้ไข: จากความอยากรู้ ...ตอนนี้ฉันได้เรียนรู้เกี่ยวกับการทำนายสาขาแล้วฉันสงสัยว่าการเปรียบเทียบแบบอะนาล็อกจะแสดงสำหรับOR b คืออะไรไม่ใช่ศูนย์:

กราฟของ a หรือ b ที่ไม่เป็นศูนย์

เราเห็นผลเช่นเดียวกันกับการคาดคะเนสาขาตามที่คาดไว้น่าสนใจกราฟค่อนข้างพลิกไปตามแกน X

ปรับปรุง

1- ฉันเพิ่ม!(a==0 || b==0)ไปยังการวิเคราะห์เพื่อดูว่าเกิดอะไรขึ้น

2- ฉันยังรวมa != 0 || b != 0, (a+b) != 0และ(a|b) != 0จากความอยากรู้หลังจากการเรียนรู้เกี่ยวกับการทำนายสาขา แต่พวกเขาจะไม่เทียบเท่ากับการแสดงออกอื่น ๆ ในเชิงตรรกะเพราะมีเพียงออร์เดอร์ b เท่านั้นที่จะต้องไม่ใช่ศูนย์ที่จะคืนค่าจริงดังนั้นจึงไม่ได้ถูกนำมาเปรียบเทียบเพื่อประสิทธิภาพการประมวลผล

3- ฉันยังเพิ่มเกณฑ์มาตรฐานที่แท้จริงที่ฉันใช้สำหรับการวิเคราะห์ซึ่งเป็นเพียงการวนซ้ำตัวแปรภายในโดยพลการ

4- บางคนแนะนำให้รวมa != 0 & b != 0เมื่อเทียบa != 0 && b != 0กับการคาดการณ์ว่ามันจะทำงานอย่างใกล้ชิดa*b != 0เพราะเราจะลบผลการทำนายสาขา ฉันไม่รู้ว่า&สามารถใช้กับตัวแปรบูลีนได้ฉันคิดว่ามันใช้สำหรับการทำงานแบบไบนารีที่มีจำนวนเต็มเท่านั้น

หมายเหตุ: ในบริบทที่ฉันกำลังพิจารณาทั้งหมดนี้ int มากเกินไม่ใช่ปัญหา แต่นั่นเป็นการพิจารณาที่สำคัญในบริบททั่วไป

CPU: Intel Core i7-3610QM @ 2.3GHz

เวอร์ชั่น Java: 1.8.0_45
สภาพแวดล้อมรันไทม์ Java (TM) SE (รุ่น 1.8.0_45-b14)
Java HotSpot (TM) เซิร์ฟเวอร์ 64 บิตแบบ VM (สร้าง 25.45-b02, โหมดผสม)


11
เกี่ยวกับif (!(a == 0 || b == 0))อะไร Microbenchmarks นั้นไม่น่าเชื่อถือและไม่น่าจะวัดได้จริง ๆ (ประมาณ 3% ฟังดูเหมือนข้อผิดพลาดเล็กน้อยสำหรับฉัน)
Elliott Frisch

9
a != 0 & b != 0หรือ
Louis Wasserman

16
การแตกแขนงนั้นช้าถ้ากิ่งที่ทำนายไว้ผิด a*b!=0มีสาขาน้อยกว่าหนึ่ง
Erwin Bolwidt

19
(1<<16) * (1<<16) == 0แต่ทั้งคู่ต่างจากศูนย์
CodesInChaos

13
@Gene: การเพิ่มประสิทธิภาพที่เสนอของคุณไม่ถูกต้อง ไม่สนใจแม้แต่ล้นa*bเป็นศูนย์ถ้าหนึ่งของaและbเป็นศูนย์; a|bเป็นศูนย์เฉพาะถ้าทั้งคู่เป็น
hmakholm ออกเดินทางจากโมนิก้า

คำตอบ:


240

ฉันไม่สนใจปัญหาที่การเปรียบเทียบของคุณอาจมีข้อบกพร่องและรับผลลัพธ์ตามมูลค่าหน้า

มันเป็นคอมไพเลอร์หรือมันเป็นระดับฮาร์ดแวร์?

หลังนั้นฉันคิดว่า:

  if (a != 0 && b != 0)

จะคอมไพล์โหลด 2 หน่วยความจำและสองสาขาตามเงื่อนไข

  if (a * b != 0)

จะคอมไพล์โหลด 2 หน่วยความจำแบบคูณและหนึ่งสาขาแบบมีเงื่อนไข

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

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

(หมายเหตุ: คำอธิบายข้างต้นมีความซับซ้อนมากเกินไปสำหรับคำอธิบายที่แม่นยำยิ่งขึ้นคุณจะต้องดูวรรณกรรมที่ได้รับจากผู้ผลิต CPU สำหรับผู้เขียนภาษาแอสเซมบลีและผู้เขียนคอมไพเลอร์หน้า Wikipedia บนPredictors สาขามีภูมิหลังที่ดี)


อย่างไรก็ตามมีสิ่งหนึ่งที่คุณต้องระวังเกี่ยวกับการเพิ่มประสิทธิภาพนี้ มีค่าใดบ้างที่a * b != 0จะให้คำตอบที่ผิด? พิจารณากรณีที่การคำนวณผลิตภัณฑ์ส่งผลให้เกิดจำนวนเต็มล้น


UPDATE

กราฟของคุณมีแนวโน้มที่จะยืนยันสิ่งที่ฉันพูด

  • นอกจากนี้ยังมีผล "การคาดคะเนสาขา" ในa * b != 0กรณีสาขาตามเงื่อนไขและสิ่งนี้จะปรากฏในกราฟ

  • หากคุณฉายเส้นโค้งเกิน 0.9 บนแกน X ดูเหมือนว่า 1) พวกเขาจะพบกันที่ประมาณ 1.0 และ 2) จุดนัดพบจะมีค่า Y เท่ากับค่า X เท่ากับ 0.0 โดยประมาณ


อัพเดท 2

ผมไม่เข้าใจว่าทำไมเส้นโค้งที่แตกต่างกันสำหรับa + b != 0และa | b != 0กรณี มีอาจจะมีบางสิ่งบางอย่างที่ฉลาดในตรรกะสาขาพยากรณ์ หรืออาจบ่งบอกถึงสิ่งอื่น

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

อย่างไรก็ตามพวกเขาทั้งสองมีความได้เปรียบของการทำงานสำหรับทุกค่าที่ไม่ใช่เชิงลบของและab


1
@DebosmitRay - 1) ไม่ควรมี SW ผลกลางจะถูกเก็บไว้ในการลงทะเบียน 2) ในกรณีที่สองมีสองสาขา: หนึ่งในการดำเนินการ "รหัส" และอื่น ๆ ifเพื่อข้ามไปยังคำสั่งต่อไปหลังจากที่
สตีเฟ่นซี

1
@StephenC คุณมีสิทธิที่จะมายุ่งเกี่ยวกับ A + B และ a | b เพราะเส้นโค้งเป็นเดียวกันผมคิดว่ามันเป็นสีที่เป็นจริงใกล้เคียง ขอโทษคนสีตาบอด!
Maljam

3
@ njzk2 จากมุมมองความน่าจะเป็นกรณีเหล่านี้ควรมีความสมมาตรตามแกนใน 50% (ความน่าจะเป็นเป็นศูนย์a&bและa|b) พวกเขาเป็น แต่ไม่ใช่อย่างสมบูรณ์นั่นคือปริศนา
Antonín Lejsek

3
@StephenC เหตุผลa*b != 0และa+b != 0เกณฑ์มาตรฐานที่แตกต่างกันเป็นเพราะa+b != 0ไม่เทียบเท่าเลยและไม่ควรได้รับการเปรียบเทียบ ตัวอย่างเช่นด้วยa = 1, b = 0นิพจน์แรกประเมินว่าเป็นเท็จ แต่ที่สองประเมินว่าเป็นจริง คูณทำหน้าที่จัดเรียงของเหมือนและผู้ประกอบการในขณะที่ทำหน้าที่เพิ่มการจัดเรียงของเหมือนหรือผู้ประกอบการ
JS1

2
@ AntonínLejsekฉันคิดว่าความน่าจะเป็นต่างกัน ถ้าคุณมีnศูนย์แล้วโอกาสของทั้งสองaและเป็นศูนย์ที่เพิ่มขึ้นกับb nในการANDดำเนินการที่มีnความน่าจะเป็นสูงกว่าหนึ่งในนั้นคือการเพิ่มที่ไม่เป็นศูนย์และเป็นไปตามเงื่อนไข สิ่งนี้ตรงกันข้ามกับการORดำเนินการ (ความน่าจะเป็นที่หนึ่งในนั้นจะเพิ่มเป็นศูนย์ด้วยn) สิ่งนี้ขึ้นอยู่กับมุมมองทางคณิตศาสตร์ ฉันไม่แน่ใจว่านั่นเป็นวิธีการทำงานของฮาร์ดแวร์หรือไม่
WYSIWYG

70

ฉันคิดว่าเกณฑ์มาตรฐานของคุณมีข้อบกพร่องบางอย่างและอาจไม่มีประโยชน์สำหรับการอนุมานเกี่ยวกับโปรแกรมจริง นี่คือความคิดของฉัน:

  • (a|b)!=0และ(a+b)!=0ทดสอบว่าค่าใดไม่ใช่ศูนย์ในขณะที่a != 0 && b != 0และ(a*b)!=0ทดสอบว่าทั้งคู่ไม่ใช่ศูนย์ ดังนั้นคุณจึงไม่ได้เปรียบเทียบเวลาของการคำนวณเพียงอย่างเดียว: หากเงื่อนไขเป็นจริงบ่อยกว่ามันจะทำให้เกิดการประหารชีวิตของifร่างกายมากขึ้นซึ่งใช้เวลามากขึ้นเช่นกัน

  • (a+b)!=0 จะทำสิ่งผิดสำหรับค่าบวกและค่าลบที่รวมเป็นศูนย์ดังนั้นคุณจึงไม่สามารถใช้งานได้ในกรณีทั่วไปแม้ว่าจะใช้งานได้ที่นี่ก็ตาม

  • ในทำนองเดียวกัน(a*b)!=0จะทำสิ่งผิดสำหรับค่าที่ล้น (ตัวอย่างแบบสุ่ม: 196608 * 327680 เป็น 0 เพราะผลลัพธ์ที่แท้จริงหารด้วย 2 32ดังนั้น 32 บิตที่ต่ำคือ 0 และบิตเหล่านั้นคือทั้งหมดที่คุณได้รับหากเป็นการintดำเนินการ)

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

  • เว้นแต่ว่า VM สามารถกำจัดการตรวจสอบขอบเขตของอาร์เรย์ได้ที่นี่มีสาขาอื่นอีกสี่สาขาในนิพจน์เพียงเนื่องจากการตรวจสอบขอบเขตและนั่นเป็นปัจจัยที่ซับซ้อนเมื่อพยายามคิดว่าเกิดอะไรขึ้นในระดับต่ำ คุณอาจได้รับผลที่แตกต่างกันถ้าคุณแยกอาร์เรย์สองมิติเป็นสองอาร์เรย์แบนการเปลี่ยนแปลงnums[0][i]และnums[1][i]การและnums0[i]nums1[i]

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

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

  • System.currentTimeMillis()อยู่ในระบบส่วนใหญ่ไม่แม่นยำมากกว่า +/- 10 ms System.nanoTime()มักจะแม่นยำมากขึ้น

มีความไม่แน่นอนมากมายและมันก็ยากที่จะพูดอะไรที่แน่นอนเกี่ยวกับการเพิ่มประสิทธิภาพแบบ micro เหล่านี้เพราะเคล็ดลับที่เร็วกว่าบน VM หรือ CPU ตัวใดตัวหนึ่งอาจช้าลงได้อีก หากใช้งาน HotSpot JVM แบบ 32 บิตแทนที่จะใช้รุ่น 64 บิตโปรดทราบว่ามันมีสองรสชาติด้วย: "ลูกค้า" VM มีการปรับแต่งที่แตกต่างกัน (อ่อนกว่า) แตกต่างเมื่อเทียบกับ VM "เซิร์ฟเวอร์"

หากคุณสามารถแยกรหัสเครื่องที่สร้างโดย VMทำเช่นนั้นแทนที่จะพยายามเดาว่ามันทำอะไร!


24

คำตอบที่นี่ดีแม้ว่าฉันมีความคิดที่อาจปรับปรุงสิ่งต่าง ๆ

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

bool aNotZero = (nums[0][i] != 0);
bool bNotZero = (nums[1][i] != 0);
if (aNotZero && bNotZero) { /* Some code */ }

มันอาจจะทำงานได้ด้วย

int a = nums[0][i];
int b = nums[1][i];
if (a != 0 && b != 0) { /* Some code */ }

เหตุผลที่เป็นไปตามกฎของการลัดวงจรถ้าบูลีนแรกเป็นเท็จไม่ควรประเมินค่าที่สอง จะต้องดำเนินการสาขาพิเศษเพื่อหลีกเลี่ยงการประเมินnums[1][i]ว่าnums[0][i]เป็นเท็จ ตอนนี้คุณอาจไม่สนใจว่าnums[1][i]จะได้รับการประเมิน แต่คอมไพเลอร์ไม่สามารถแน่ใจได้ว่ามันจะไม่ทิ้งช่วงหรืออ้างอิงโมฆะเมื่อคุณทำ ด้วยการลดการบล็อก if to bools อย่างง่ายคอมไพเลอร์อาจฉลาดพอที่จะรู้ว่าการประเมินบูลีนที่สองโดยไม่จำเป็นจะไม่มีผลข้างเคียง


3
upvoted แต่ผมมีความรู้สึกนี้ไม่ได้ค่อนข้างตอบคำถาม
Pierre Arlaud

3
นั่นเป็นวิธีที่จะแนะนำสาขาโดยไม่เปลี่ยนตรรกะจากการไม่ได้สาขา (ถ้าวิธีที่คุณได้รับaและbมีผลข้างเคียงที่คุณจะต้องเก็บไว้) คุณยังมี&&ดังนั้นคุณยังมีสาขา
Jon Hanna

11

เมื่อเราทำการคูณแม้ว่าตัวเลขหนึ่งเป็น 0 แล้วผลคูณคือ 0 ขณะเขียน

    (a*b != 0)

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

   (a != 0 && b != 0)

โดยที่ทุกองค์ประกอบเปรียบเทียบกับ 0 และประเมินผล ดังนั้นเวลาที่ใช้จึงน้อยลง แต่ฉันเชื่อว่าเงื่อนไขที่สองอาจช่วยให้คุณแก้ไขปัญหาได้แม่นยำยิ่งขึ้น


4
ในการแสดงออกที่สองถ้าaเป็นศูนย์แล้วbไม่จำเป็นต้องได้รับการประเมินเนื่องจากการแสดงออกทั้งหมดเป็นเท็จแล้ว ดังนั้นทุกองค์ประกอบเปรียบเทียบไม่เป็นความจริง
Kuba Wyrostek

9

คุณกำลังใช้ข้อมูลอินพุตแบบสุ่มซึ่งทำให้สาขาไม่สามารถคาดเดาได้ ในทางปฏิบัติสาขามักจะคาดการณ์ได้ (~ 90%) ดังนั้นในรหัสจริงรหัสสาขามีแนวโน้มว่าจะเร็วขึ้น

ที่กล่าวว่า ผมไม่เห็นว่าอาจจะเร็วกว่าa*b != 0 (a|b) != 0โดยทั่วไปการคูณจำนวนเต็มมีราคาแพงกว่าบิตหรือ แต่สิ่งต่าง ๆ เช่นนี้บางครั้งก็แปลก ดูตัวอย่าง "ตัวอย่างที่ 7: ความซับซ้อน Hardware" เช่นจากแกลลอรี่ของตัวประมวลผลผลกระทบแคช


2
&ไม่ได้เป็น "OR ระดับบิต" แต่ (ในกรณีนี้) เป็น "ตรรกะและ" เพราะทั้งสองตัวถูกดำเนินการเป็นบูลีนและก็ไม่ได้|;-)
siegi

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