ความแตกต่างระหว่าง if (a - b <0) และ if (a <b)


252

ฉันอ่านArrayListซอร์สโค้ดของ Java และสังเกตเห็นการเปรียบเทียบบางอย่างในคำสั่ง if

ใน Java 7 วิธีgrow(int)ใช้

if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

ใน Java 6 growไม่มีอยู่จริง ensureCapacity(int)อย่างไรก็ตามวิธีการใช้

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

อะไรคือเหตุผลที่อยู่เบื้องหลังการเปลี่ยนแปลง? มันเป็นปัญหาด้านประสิทธิภาพหรือเป็นสไตล์หรือไม่?

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


35
@Tunaki เป็นอย่างไรif (newCapacity - minCapacity < 0)ดีกว่าif (newCapacity < minCapacity)ในแง่ของการป้องกันการล้น?
Eran

3
ฉันสงสัยว่าสัญญาณที่กล่าวถึงมากเกินจริงนั้นเป็นเหตุผลหรือไม่ การลบดูเหมือนจะเป็นทางเลือกสำหรับการล้น องค์ประกอบอาจจะพูดว่า "สิ่งนี้จะไม่ล้น" อาจเป็นตัวแปรทั้งสองที่ไม่ใช่ลบ
Joop Eggen

12
FYI คุณเชื่อว่าการทำการเปรียบเทียบนั้นเร็วกว่าการทำการ "ลบอย่างสมบูรณ์" จากประสบการณ์ของฉันที่ระดับรหัสเครื่องมักจะทำการเปรียบเทียบโดยทำการลบลบทิ้งผลลัพธ์และตรวจสอบค่าสถานะผลลัพธ์
David Dubois

6
@David Dubois: OP ไม่คิดว่าการเปรียบเทียบนั้นเร็วกว่าการลบ แต่การเปรียบเทียบด้วยศูนย์อาจจะเร็วกว่าการเปรียบเทียบค่าสองค่าใด ๆ และยังถือว่าอย่างถูกต้องว่าจะไม่ถือเมื่อคุณทำการลบจริงก่อน เพื่อรับค่าเพื่อเปรียบเทียบกับศูนย์ นั่นคือทั้งหมดที่สมเหตุสมผล
Holger

คำตอบ:


285

a < bและa - b < 0อาจหมายถึงสองสิ่งที่แตกต่างกัน พิจารณารหัสต่อไปนี้:

int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
    System.out.println("a < b");
}
if (a - b < 0) {
    System.out.println("a - b < 0");
}

a - b < 0เมื่อทำงานนี้เท่านั้นที่จะพิมพ์ สิ่งที่เกิดขึ้นนั้นเป็นสิ่งที่a < bผิดอย่างชัดเจน แต่มีมากเกินa - bและกลายเป็น-1สิ่งที่เป็นลบ

Integer.MAX_VALUEตอนนี้ต้องบอกว่าพิจารณาว่าอาร์เรย์มีความยาวที่เป็นจริงใกล้เคียงกับ รหัสArrayListจะเป็นดังนี้:

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

oldCapacityมันใกล้กับInteger.MAX_VALUEดังนั้นnewCapacity(ซึ่งเป็นoldCapacity + 0.5 * oldCapacity) อาจล้นและกลายเป็นInteger.MIN_VALUE(เช่นลบ) จากนั้นการลบminCapacity อันเดอร์โฟลว์กลับไปเป็นจำนวนบวก

การตรวจสอบนี้ช่วยให้มั่นใจifว่าไม่ได้ดำเนินการ ถ้ารหัสถูกเขียนเป็นif (newCapacity < minCapacity)ก็จะเป็นtrueในกรณีนี้ (ตั้งแต่newCapacityเป็นลบ) ดังนั้นnewCapacityจะถูกบังคับให้โดยไม่คำนึงถึงminCapacityoldCapacity

กรณีโอเวอร์โฟลว์นี้ได้รับการจัดการโดยถัดไปหาก เมื่อnewCapacityมี overflowed นี้จะtrue: MAX_ARRAY_SIZEถูกกำหนดให้เป็นInteger.MAX_VALUE - 8และเป็นInteger.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0 จึงถูกต้องจัดการ: วิธีการส่งกลับหรือtruenewCapacityhugeCapacityMAX_ARRAY_SIZEInteger.MAX_VALUE

หมายเหตุ: นี่คือสิ่งที่// overflow-conscious codeความคิดเห็นในวิธีนี้พูด


8
การสาธิตที่ดีเกี่ยวกับความแตกต่างระหว่างคณิตศาสตร์และ CS
piggybox

36
@piggybox ฉันจะไม่พูดอย่างนั้น นี่คือคณิตศาสตร์ มันไม่ใช่คณิตศาสตร์ใน Z แต่ในรุ่นจำนวนเต็มโมดูโล 2 ^ 32 (ด้วยการรับรองแบบบัญญัติที่เลือกแตกต่างจากปกติ) มันเป็นระบบคณิตศาสตร์ที่เหมาะสมไม่ใช่แค่ "คอมพิวเตอร์ lol และนิสัยใจคอ"
แฮโรลด์

2
ฉันจะเขียนโค้ดที่ไม่ได้ล้นเลย
Aleksandr Dubinsky

โปรเซสเซอร์ IIRC ใช้น้อยกว่าการเรียนการสอนในจำนวนเต็มลงนามโดยการทำและการตรวจสอบถ้าบิตด้านบนเป็นa - b 1พวกเขาจัดการกับน้ำล้นได้อย่างไร
Ben Leggiero

2
@ BenC.R.Leggiero x86 และอื่น ๆ ติดตามเงื่อนไขต่างๆผ่านการตั้งค่าสถานะในการลงทะเบียนแยกต่างหากเพื่อใช้กับคำแนะนำแบบมีเงื่อนไข รีจิสเตอร์นี้มีบิตแยกต่างหากสำหรับสัญลักษณ์ของผลลัพธ์ zeroness ของผลลัพธ์และการเกิด over- / underflow ในการดำเนินการเกี่ยวกับคณิตศาสตร์ครั้งสุดท้าย

105

ฉันพบคำอธิบายนี้ :

ในวันอังคารที่ 9 มีนาคม 2010 เวลา 03:02 น. Kevin L. Stern เขียนว่า:

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

if (a - b > 0)

ถึง

if (a > b)

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

if (oldCapacity > RESIZE_OVERFLOW_THRESHOLD) {
   // Do something
} else {
  // Do something else
}

มันเป็นจุดที่ดี

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

API ปัจจุบันใช้ดังนี้:

int newcount = count + len;
ensureCapacity(newcount);

หากคุณต้องการหลีกเลี่ยงการล้นคุณจะต้องเปลี่ยนไปใช้สิ่งที่เป็นธรรมชาติน้อยลงเช่น

ensureCapacity(count, len);
int newcount = count + len;

อย่างไรก็ตามฉันกำลังเก็บรหัสที่เกินความจำเป็น แต่เพิ่มความคิดเห็นคำเตือนเพิ่มเติมและ "สร้างแถว" การสร้างอาร์เรย์ขนาดใหญ่เพื่อให้ ArrayListรหัสของตอนนี้ดูเหมือนว่า:

/**
 * Increases the capacity of this <tt>ArrayList</tt> instance, if
 * necessary, to ensure that it can hold at least the number of elements
 * specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
public void ensureCapacity(int minCapacity) {
    modCount++;

    // Overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // Overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

สร้าง Webrev ใหม่แล้ว

นกนางแอ่น

ใน Java 6 หากคุณใช้ API เป็น:

int newcount = count + len;
ensureCapacity(newcount);

และnewCountล้น (นี้จะกลายเป็นลบ) if (minCapacity > oldCapacity)จะกลับเท็จและคุณอาจเข้าใจผิดคิดว่าเพิ่มขึ้นArrayListlen


2
ความคิดที่ดี แต่มันขัดแย้งกับการใช้ensureCapacity ; ถ้าminCapacityเป็นลบคุณจะไม่ไปถึงจุดนั้น - เป็นเรื่องที่ไม่สนใจอย่างเงียบ ๆ เนื่องจากการใช้งานที่ซับซ้อนทำเพื่อป้องกัน ดังนั้น“ เราไม่สามารถทำสิ่งนี้ได้” เพื่อความเข้ากันได้กับสาธารณะ API จึงเป็นข้อโต้แย้งที่แปลก ๆ ผู้โทรเท่านั้นที่พึ่งพาพฤติกรรมนี้เป็นผู้โทรภายใน
Holger

1
@Holger หากminCapacityเป็นค่าลบมาก (เช่นเกิดจากการintโอเวอร์โฟลว์เมื่อเพิ่มขนาดปัจจุบันของ ArrayList ให้กับจำนวนองค์ประกอบที่คุณต้องการเพิ่ม) minCapacity - elementData.lengthเพื่อล้นอีกครั้งและกลายเป็นค่าบวก นั่นเป็นวิธีที่ฉันเข้าใจ
Eran

1
@ Holger อย่างไรก็ตามพวกเขาเปลี่ยนมันอีกครั้งใน Java 8 เป็นif (minCapacity > minExpand)ซึ่งฉันไม่เข้าใจ
Eran

ใช่ทั้งสองaddAllวิธีเป็นกรณีเดียวที่เกี่ยวข้องเนื่องจากผลรวมของขนาดปัจจุบันและจำนวนองค์ประกอบใหม่สามารถล้นได้ อย่างไรก็ตามสิ่งเหล่านี้คือการโทรภายในและอาร์กิวเมนต์“ เราไม่สามารถเปลี่ยนได้เพราะensureCapacityเป็น API สาธารณะ” เป็นอาร์กิวเมนต์แปลก ๆ เมื่อจริง ๆ แล้วensureCapacityไม่สนใจค่าลบ Java 8 API ไม่ได้เปลี่ยนลักษณะการทำงานสิ่งที่มันทำคือการละเว้นความจุต่ำกว่าความจุเริ่มต้นเมื่อArrayListอยู่ในสถานะเริ่มต้น (เช่นเริ่มต้นด้วยความจุเริ่มต้นและยังว่างเปล่า)
Holger

กล่าวอีกนัยหนึ่งเหตุผลเกี่ยวกับnewcount = count + lenถูกต้องเมื่อมันมาถึงการใช้งานภายใน แต่มันไม่ได้ใช้กับpublicวิธีการensureCapacity()...
Holger

19

ดูรหัส:

int newCapacity = oldCapacity + (oldCapacity >> 1);

หากoldCapacityมีขนาดค่อนข้างใหญ่จะทำให้เกิดการล้นและnewCapacityจะเป็นจำนวนลบ การเปรียบเทียบเช่นnewCapacity < oldCapacityจะประเมินอย่างไม่ถูกต้องtrueและArrayListจะไม่เติบโต

แต่รหัสเป็นลายลักษณ์อักษร ( newCapacity - minCapacity < 0ผลตอบแทนเท็จ) จะช่วยให้ค่าเชิงลบของการnewCapacityที่จะได้รับการประเมินต่อไปในบรรทัดถัดไปมีผลในการคำนวณnewCapacityโดยการเรียกhugeCapacity( newCapacity = hugeCapacity(minCapacity);) เพื่อให้สามารถที่จะเติบโตขึ้นไปArrayListMAX_ARRAY_SIZE

นี่คือสิ่งที่// overflow-conscious codeความคิดเห็นพยายามสื่อสารแม้ว่าจะค่อนข้างเอียง

ดังนั้นบรรทัดล่างการเปรียบเทียบใหม่จะช่วยป้องกันการจัดสรรArrayListขนาดใหญ่กว่าที่กำหนดไว้ล่วงหน้าMAX_ARRAY_SIZEในขณะที่ช่วยให้มันเติบโตขึ้นจนถึงขีด จำกัด นั้นหากจำเป็น


1

แบบฟอร์มทั้งสองนั้นทำงานเหมือนกันทุกประการเว้นแต่นิพจน์จะa - bล้นซึ่งในกรณีนี้มันจะตรงกันข้ามกัน หากaเป็นค่าลบขนาดใหญ่และbเป็นค่าบวกจำนวนมากนั้น(a < b)จะเป็นจริงอย่างชัดเจน แต่a - bจะมีค่าเกินที่จะกลายเป็นค่าบวกดังนั้นจึง(a - b < 0)เป็นเท็จ

หากคุณคุ้นเคยกับแอสเซมบลี x86 ให้พิจารณาว่า(a < b)มีการใช้งานโดย a jgeซึ่งจะแยกไปรอบ ๆ เนื้อความของคำสั่ง if เมื่อ SF = OF ในทางกลับกัน(a - b < 0)จะทำหน้าที่เหมือน a jnsซึ่งแยกเมื่อ SF = 0 ดังนั้นสิ่งเหล่านี้จะทำงานแตกต่างกันอย่างแม่นยำเมื่อ OF = 1

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