Sự khác biệt giữa if (a - b <0) và if (a <b)


252

Tôi đã đọc ArrayListmã nguồn của Java và nhận thấy một số so sánh trong câu lệnh if.

Trong Java 7, phương thức grow(int)sử dụng

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

Trong Java 6, growkhông tồn tại. Phương pháp ensureCapacity(int)tuy nhiên sử dụng

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

Lý do đằng sau sự thay đổi là gì? Đó có phải là một vấn đề hiệu suất hay chỉ là một phong cách?

Tôi có thể tưởng tượng rằng so sánh với số 0 thì nhanh hơn, nhưng thực hiện phép trừ hoàn toàn chỉ để kiểm tra xem nó có âm hơi quá mức đối với tôi không. Ngoài ra, về mặt mã byte, điều này sẽ liên quan đến hai hướng dẫn ( ISUBIF_ICMPGE) thay vì một ( IFGE).


35
@Tunaki Làm thế nào if (newCapacity - minCapacity < 0)tốt hơn if (newCapacity < minCapacity)về mặt ngăn chặn tràn?
Eran

3
Tôi tự hỏi liệu các dấu hiệu tràn được đề cập thực sự là lý do. Phép trừ có vẻ nhiều hơn một ứng cử viên cho tràn. Thành phần có thể nói "tuy nhiên điều này sẽ không tràn", có thể cả hai biến đều không âm.
Eggen

12
FYI, bạn tin rằng thực hiện so sánh nhanh hơn thực hiện "trừ hoàn toàn". Theo kinh nghiệm của tôi, ở cấp mã máy, thông thường các phép so sánh được thực hiện bằng cách thực hiện phép trừ, vứt bỏ kết quả và kiểm tra các cờ kết quả.
David Dubois

6
@David Dubois: OP không cho rằng so sánh nhanh hơn phép trừ, nhưng so sánh với 0 có thể nhanh hơn so với hai giá trị tùy ý và cũng giả định chính xác rằng điều này không giữ khi bạn thực hiện phép trừ thực tế trước để có được một giá trị để so sánh với không. Đó là tất cả khá hợp lý.
Holger

Câu trả lời:


285

a < ba - b < 0có thể có nghĩa là hai điều khác nhau. Hãy xem xét các mã sau đây:

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");
}

Khi chạy, điều này sẽ chỉ in a - b < 0. Điều gì xảy ra là a < brõ ràng là sai, nhưng a - btràn ra và trở thành -1, đó là tiêu cực.

Bây giờ, đã nói rằng, xem xét rằng mảng có độ dài thực sự gần Integer.MAX_VALUE. Các mã trong ArrayListnhư thế này:

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

oldCapacitylà thực sự gần với Integer.MAX_VALUEnhư vậy newCapacity(đó là oldCapacity + 0.5 * oldCapacity) có thể tràn và trở thành Integer.MIN_VALUE(tức là tiêu cực). Sau đó, trừ minCapacity underflows sao vào một số dương.

Kiểm tra này đảm bảo rằng ifkhông được thực hiện. Nếu mã được viết là if (newCapacity < minCapacity), nó sẽ làtrue trong trường hợp này (vì newCapacitylà âm) nên newCapacitysẽ bị buộc minCapacitybất kể oldCapacity.

Trường hợp tràn này được xử lý bởi tiếp theo nếu. Khi newCapacityđã tràn, đây sẽ là true: MAX_ARRAY_SIZEđược định nghĩa là Integer.MAX_VALUE - 8Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0true. CácnewCapacity do đó một cách đúng đắn xử lý: hugeCapacitytrở về phương pháp MAX_ARRAY_SIZEhay Integer.MAX_VALUE.

NB: đây là những gì // overflow-conscious codenhận xét trong phương pháp này đang nói.


8
Bản demo tốt về sự khác biệt giữa toán học và CS
piggybox

36
@piggybox Tôi sẽ không nói như vậy. Đây là toán học. Nó không chỉ là toán học trong Z, mà là một phiên bản của các số nguyên modulo 2 ^ 32 (với các biểu diễn chính tắc được chọn khác với thông thường). Đó là một hệ thống toán học phù hợp, không chỉ là "máy tính lol và những điều kỳ quặc".
harold

2
Tôi đã viết mã mà không tràn.
Alexanderr Dubinsky

Bộ xử lý IIRC thực hiện một lệnh nhỏ hơn các số nguyên đã ký bằng cách thực hiện a - bvà kiểm tra xem bit trên cùng có phải là a không 1. Làm thế nào để họ xử lý tràn?
Ben Leggiero

2
@ BenC.R.Leggiero x86, trong số những người khác, theo dõi các điều kiện khác nhau thông qua các cờ trạng thái trong một thanh ghi riêng để sử dụng với các hướng dẫn có điều kiện. Thanh ghi này có các bit riêng biệt cho dấu hiệu của kết quả, số không của kết quả và liệu có xảy ra quá dòng / dưới dòng trong hoạt động đối xứng cuối cùng hay không.

105

Tôi tìm thấy lời giải thích này :

Trên Tue, ngày 9 tháng 3 năm 2010 lúc 03:02, Kevin L. Stern đã viết:

Tôi đã thực hiện một tìm kiếm nhanh và có vẻ như Java thực sự là phần bổ sung của hai. Tuy nhiên, xin vui lòng cho phép tôi chỉ ra rằng, nói chung, loại mã này làm tôi lo lắng vì tôi hoàn toàn mong đợi rằng đến một lúc nào đó sẽ có người đi cùng và làm chính xác những gì Dmytro đề xuất; nghĩa là, ai đó sẽ thay đổi:

if (a - b > 0)

đến

if (a > b)

và toàn bộ con tàu sẽ chìm. Cá nhân tôi muốn tránh những điều tối nghĩa như làm cho số nguyên tràn ra làm cơ sở thiết yếu cho thuật toán của tôi trừ khi có lý do chính đáng để làm như vậy. Nói chung, tôi muốn tránh tràn hoàn toàn và làm cho kịch bản tràn hơn rõ ràng hơn:

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

Đó là một điểm tốt.

Trong ArrayListchúng tôi không thể làm điều này (hoặc ít nhất là không tương thích), vì ensureCapacitylà API công khai và thực sự đã chấp nhận các số âm là yêu cầu cho khả năng tích cực không thể thỏa mãn.

API hiện tại được sử dụng như thế này:

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

Nếu bạn muốn tránh tràn, bạn sẽ cần thay đổi thành một thứ ít tự nhiên hơn như

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

Dù sao, tôi đang giữ mã có ý thức tràn, nhưng thêm nhiều bình luận cảnh báo và tạo ra mảng lớn "ngoài luồng" để ArrayListmã đó giờ trông như sau:

/**
 * 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 được tái sinh.

Martin

Trong Java 6, nếu bạn sử dụng API như:

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

newCounttràn (điều này trở thành tiêu cực), if (minCapacity > oldCapacity)sẽ trả về sai và bạn có thể nhầm tưởng rằng ArrayListđã tăng lên len.


2
Ý tưởng hay nhưng nó bị mâu thuẫn bởi việc thực hiệnensureCapacity ; nếu minCapacitylà tiêu cực, bạn sẽ không bao giờ đến điểm đó. Nó chỉ âm thầm bị bỏ qua như việc triển khai phức tạp giả vờ ngăn chặn. Vì vậy, chúng tôi không thể thực hiện điều này vì tính tương thích API công khai là một đối số lạ như họ đã làm. Những người gọi duy nhất dựa vào hành vi này là những người nội bộ.
Holger

1
@Holger Nếu minCapacityrất âm (nghĩa là kết quả từ inttràn khi thêm kích thước hiện tại của ArrayList vào số phần tử bạn muốn thêm), minCapacity - elementData.lengthđể tràn lại và trở nên tích cực. Đó là cách tôi hiểu nó.
Eran

1
@Holger Tuy nhiên, họ đã thay đổi nó một lần nữa trong Java 8 if (minCapacity > minExpand), mà tôi không hiểu.
Eran

Có, hai addAllphương thức là trường hợp duy nhất có liên quan vì tổng kích thước hiện tại và số lượng phần tử mới có thể tràn. Tuy nhiên, đây là các cuộc gọi nội bộ và đối số, chúng ta không thể thay đổi nó bởi vì ensureCapacityAPI API công khai là một đối số lạ khi trên thực tế, ensureCapacitykhông bỏ qua các giá trị âm. API Java 8 đã không thay đổi hành vi đó, tất cả những gì nó làm là bỏ qua các dung lượng dưới dung lượng mặc định khi ArrayListở trạng thái ban đầu (nghĩa là được khởi tạo với dung lượng mặc định và vẫn trống).
Holger

Nói cách khác, các lập luận về newcount = count + lenlà đúng khi nói đến việc sử dụng nội bộ, tuy nhiên, nó không áp dụng cho các publicphương pháp ensureCapacity()...
Holger

19

Nhìn vào mã:

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

Nếu oldCapacitykhá lớn, nó sẽ tràn ra, và newCapacitysẽ là một số âm. Một so sánh như thế newCapacity < oldCapacitysẽ đánh giá không chính xác trueArrayListsẽ không phát triển.

Thay vào đó, mã như được viết ( newCapacity - minCapacity < 0trả về false) sẽ cho phép giá trị âm newCapacityđược đánh giá thêm trong dòng tiếp theo, dẫn đến việc tính toán lại newCapacitybằng cách gọi hugeCapacity( newCapacity = hugeCapacity(minCapacity);) để cho phép ArrayListtăng lên MAX_ARRAY_SIZE.

Đây là những gì // overflow-conscious codebình luận đang cố gắng truyền đạt, mặc dù khá xiên.

Vì vậy, điểm mấu chốt, so sánh mới bảo vệ chống phân bổ ArrayListlớn hơn mức được xác định trước MAX_ARRAY_SIZEtrong khi cho phép nó phát triển đúng đến giới hạn đó nếu cần.


1

Hai hình thức hành xử giống hệt nhau trừ khi biểu thức a - btràn ra, trong trường hợp chúng ngược nhau. Nếu alà một tiêu cực lớn, và blà một tích cực lớn, thì (a < b)rõ ràng là đúng, nhưng a - bsẽ tràn ra để trở thành tích cực, vì vậy (a - b < 0)là sai.

Nếu bạn quen thuộc với mã lắp ráp x86, hãy xem xét điều đó (a < b)được thực hiện bởi a jge, nhánh này bao quanh phần thân của câu lệnh if khi SF = OF. Mặt khác, (a - b < 0)sẽ hoạt động như a jns, phân nhánh khi SF = 0. Do đó, các hành vi này hoạt động chính xác khác nhau khi OF = 1.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.