(A + B + C) ≠ (A + C + B) và sắp xếp lại trình biên dịch


108

Thêm hai số nguyên 32 bit có thể dẫn đến tràn số nguyên:

uint64_t u64_z = u32_x + u32_y;

Có thể tránh được hiện tượng tràn này nếu một trong các số nguyên 32 bit lần đầu tiên được ép hoặc thêm vào số nguyên 64 bit.

uint64_t u64_z = u32_x + u64_a + u32_y;

Tuy nhiên, nếu trình biên dịch quyết định sắp xếp lại thứ tự bổ sung:

uint64_t u64_z = u32_x + u32_y + u64_a;

sự tràn số nguyên vẫn có thể xảy ra.

Các trình biên dịch có được phép thực hiện việc sắp xếp lại như vậy hay chúng ta có thể tin tưởng họ nhận thấy sự không nhất quán của kết quả và giữ nguyên thứ tự biểu thức không?


15
Bạn không thực sự hiển thị tràn số nguyên bởi vì bạn có vẻ là uint32_tcác giá trị được thêm vào - những giá trị không bị tràn, chúng bao bọc. Đây không phải là những hành vi khác nhau.
Martin Bonner hỗ trợ Monica

5
Xem phần 1.9 của các tiêu chuẩn c ++, nó trực tiếp trả lời câu hỏi của bạn (thậm chí có một ví dụ gần giống hệt như của bạn).
Holt

3
@Tal: Như những người khác đã nói: không có tràn số nguyên. Chưa ký được định nghĩa là quấn, vì đã ký thì đó là hành vi không xác định, vì vậy bất kỳ quá trình triển khai nào cũng sẽ làm được, kể cả daemons mũi.
quá trung thực cho trang web này

5
@Tal: Vớ vẩn! Như tôi đã viết: tiêu chuẩn là rất rõ ràng và đòi hỏi gói, không bão hòa (mà có thể có thể với ký, vì đó là UB như-của tiêu chuẩn.
quá trung thực cho trang web này

15
@rustyx: Cho dù bạn gọi nó là gói hay tràn, điểm vẫn là ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0kết quả 0, ngược lại (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)kết quả là 0x100000000và hai giá trị này không bằng nhau. Vì vậy, việc trình biên dịch có thể áp dụng phép biến đổi đó hay không là rất quan trọng. Nhưng đúng vậy, tiêu chuẩn chỉ sử dụng từ "tràn" cho các số nguyên có dấu, không có dấu.
Steve Jessop

Câu trả lời:


84

Nếu trình tối ưu hóa thực hiện việc sắp xếp lại như vậy thì nó vẫn bị ràng buộc với đặc điểm kỹ thuật C, vì vậy việc sắp xếp lại như vậy sẽ trở thành:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Cơ sở lý luận:

Chúng tôi bắt đầu với

uint64_t u64_z = u32_x + u64_a + u32_y;

Phép cộng được thực hiện từ trái sang phải.

Các quy tắc thăng hạng số nguyên cho biết rằng trong phép cộng đầu tiên trong biểu thức ban đầu, u32_xđược thăng hạng lên uint64_t. Trong lần bổ sung thứ hai, u32_ycũng sẽ được thăng cấp uint64_t.

Vì vậy, để tuân thủ đặc điểm kỹ thuật C, bất kỳ trình tối ưu hóa nào cũng phải quảng bá u32_xu32_ytới các giá trị không dấu 64 bit. Điều này tương đương với việc thêm một dàn diễn viên. (Việc tối ưu hóa thực tế không được thực hiện ở cấp độ C, nhưng tôi sử dụng ký hiệu C vì đó là ký hiệu mà chúng tôi hiểu.)


Nó không phải là liên kết trái, vì vậy (u32_x + u32_t) + u64_a?
Vô dụng

12
@Useless: Klas đã truyền mọi thứ thành 64 bit. Bây giờ đơn đặt hàng không có bất kỳ sự khác biệt nào cả. Trình biên dịch không cần tuân theo thuyết kết hợp, nó chỉ cần tạo ra kết quả chính xác như thể nó đã làm.
gnasher729,

2
Có vẻ như cho thấy rằng mã của OP sẽ được đánh giá như vậy, điều này không đúng.
Vô dụng

@Klas - chăm sóc để giải thích lý do tại sao đây là trường hợp và cách chính xác bạn đến mẫu mã của bạn?
gỉ vào

1
@rustyx Nó cần một lời giải thích. Cảm ơn vì đã thúc đẩy tôi thêm một.
Klas Lindbäck,

28

Một trình biên dịch chỉ được phép sắp xếp lại theo quy tắc as if . Nghĩa là, nếu việc sắp xếp lại sẽ luôn cho kết quả giống như thứ tự đã chỉ định, thì nó được phép. Nếu không (như trong ví dụ của bạn), thì không.

Ví dụ, cho biểu thức sau

i32big1 - i32big2 + i32small

đã được xây dựng cẩn thận để trừ hai giá trị được biết là lớn nhưng giống nhau, và chỉ sau đó thêm giá trị nhỏ khác (do đó tránh bất kỳ sự tràn nào), trình biên dịch có thể chọn sắp xếp lại thành:

(i32small - i32big2) + i32big1

và dựa trên thực tế là nền tảng mục tiêu đang sử dụng số học hai phần bù với kết quả tổng hợp để ngăn chặn các vấn đề. (Việc sắp xếp lại thứ tự như vậy có thể hợp lý nếu trình biên dịch được nhấn cho các thanh ghi và tình cờ đã có i32smalltrong một thanh ghi).


Ví dụ của OP sử dụng các kiểu không dấu. i32big1 - i32big2 + i32smallngụ ý các loại đã ký. Các mối quan tâm bổ sung có hiệu lực.
Chux - Khôi phục Monica

@chux Hoàn toàn đúng. Điểm tôi đang cố gắng thực hiện là mặc dù tôi không thể viết (i32small-i32big2) + i32big1, (vì nó có thể gây ra UB), trình biên dịch có thể sắp xếp lại nó một cách hiệu quả vì trình biên dịch có thể tự tin rằng hành vi sẽ đúng.
Martin Bonner ủng hộ Monica vào

3
@chux: Các mối quan tâm khác như UB không có tác dụng, vì chúng ta đang nói về một trình biên dịch sắp xếp lại thứ tự theo quy tắc as-if. Một trình biên dịch cụ thể có thể tận dụng lợi thế của việc biết hành vi tràn của chính nó.
MSalters

16

Có quy tắc "như thể" trong C, C ++ và Objective-C: Trình biên dịch có thể làm bất cứ điều gì nó thích miễn là không có chương trình phù hợp nào có thể phân biệt được sự khác biệt.

Trong các ngôn ngữ này, a + b + c được định nghĩa giống như (a + b) + c. Nếu bạn có thể cho biết sự khác biệt giữa điều này và ví dụ a + (b + c) thì trình biên dịch không thể thay đổi thứ tự. Nếu bạn không thể phân biệt được sự khác biệt, thì trình biên dịch có thể tự do thay đổi thứ tự, nhưng điều đó không sao cả, vì bạn không thể phân biệt được sự khác biệt.

Trong ví dụ của bạn, với b = 64 bit, a và c 32 bit, trình biên dịch sẽ được phép đánh giá (b + a) + c hoặc thậm chí (b + c) + a, bởi vì bạn không thể phân biệt được, nhưng không phải (a + c) + b vì bạn có thể phân biệt được.

Nói cách khác, trình biên dịch không được phép làm bất cứ điều gì khiến mã của bạn hoạt động khác với những gì nó cần. Không bắt buộc phải tạo ra mã mà bạn nghĩ rằng nó sẽ tạo ra hoặc bạn nghĩ rằng nó nên tạo ra, nhưng mã sẽ cung cấp cho bạn chính xác kết quả mà nó cần.


Nhưng với một cảnh báo lớn; trình biên dịch được tự do giả định không có hành vi không xác định (trong trường hợp này là tràn). Điều này tương tự như cách if (a + 1 < a)tối ưu hóa kiểm tra tràn .
csiz

7
@csiz ... trên các biến đã ký . Các biến không dấu có ngữ nghĩa tràn được xác định rõ ràng (quấn quanh).
Gavin S. Yancey,

7

Trích dẫn từ các tiêu chuẩn :

[Lưu ý: Các toán tử chỉ có thể được nhóm lại theo các quy tắc toán học thông thường khi các toán tử thực sự là liên kết hoặc giao hoán. 7 Ví dụ, trong đoạn sau int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

câu lệnh biểu thức hoạt động giống hệt như

a = (((a + 32760) + b) + 5);

do tính liên kết và ưu tiên của các toán tử này. Do đó, kết quả của tổng (a + 32760) tiếp theo được cộng vào b, và kết quả đó sau đó được cộng với 5 dẫn đến giá trị được gán cho a. Trên một máy trong đó tràn tạo ra một ngoại lệ và trong đó phạm vi giá trị có thể biểu diễn bởi một int là [-32768, + 32767], việc triển khai không thể viết lại biểu thức này thành

a = ((a + b) + 32765);

vì nếu giá trị của a và b tương ứng là -32754 và -15, thì tổng a + b sẽ tạo ra một ngoại lệ trong khi biểu thức ban đầu thì không; cũng không thể viết lại biểu thức dưới dạng

a = ((a + 32765) + b);

hoặc là

a = (a + (b + 32765));

vì các giá trị của a và b có thể lần lượt là 4 và -8 hoặc -17 và 12. Tuy nhiên, trên một máy tính trong đó lỗi tràn không tạo ra ngoại lệ và trong đó kết quả của tràn có thể đảo ngược, câu lệnh biểu thức trên có thể được viết lại bởi việc thực hiện theo bất kỳ cách nào ở trên vì kết quả tương tự sẽ xảy ra. - ghi chú cuối]


4

Các trình biên dịch có được phép thực hiện việc sắp xếp lại như vậy hay chúng ta có thể tin tưởng họ nhận thấy sự không nhất quán của kết quả và giữ nguyên thứ tự biểu thức không?

Trình biên dịch chỉ có thể sắp xếp lại thứ tự nếu nó cho cùng một kết quả - ở đây, như bạn đã quan sát, thì không.


Có thể viết một mẫu hàm, nếu bạn muốn, mẫu này thúc đẩy tất cả các đối số std::common_typetrước khi thêm - điều này sẽ an toàn và không dựa vào thứ tự đối số hoặc ép kiểu thủ công, nhưng nó khá rắc rối.


Tôi biết nên sử dụng ép kiểu rõ ràng, nhưng tôi muốn biết hành vi của trình biên dịch khi quá trình truyền như vậy bị bỏ qua một cách nhầm lẫn.
Tal,

1
Như tôi đã nói, không có phép ép kiểu rõ ràng: phép cộng bên trái được thực hiện trước tiên, không có quảng cáo tích hợp, và do đó phải tuân theo gói. Các kết quả bổ sung rằng, có thể bao bọc, được rồi thăng uint64_tđể bổ sung vào giá trị phải nhất.
Vô ích

Giải thích của bạn về quy tắc as-if là hoàn toàn sai. Ví dụ, ngôn ngữ C chỉ định những thao tác nào sẽ xảy ra trên một máy trừu tượng. Quy tắc "as-if" cho phép nó hoàn toàn làm bất cứ điều gì nó muốn miễn là không ai có thể phân biệt được.
gnasher729,

Có nghĩa là trình biên dịch có thể làm bất cứ điều gì nó muốn miễn là kết quả giống với kết quả được xác định bởi các quy tắc chuyển đổi số học và kết hợp trái được hiển thị.
Vô dụng

1

Nó phụ thuộc vào chiều rộng bit của unsigned/int.

2 bên dưới không giống nhau (khi unsigned <= 32bit). u32_x + u32_ytrở thành 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Chúng giống nhau (khi unsigned >= 34bit). Quảng cáo số nguyên khiến phép u32_x + u32_ycộng xảy ra ở phép toán 64-bit. Thứ tự không liên quan.

Nó là UB (khi unsigned == 33bit). Khuyến mãi số nguyên khiến phép cộng xảy ra ở phép toán 33-bit có dấu và phần tràn có dấu là UB.

Các trình biên dịch có được phép thực hiện việc sắp xếp lại như vậy không ...?

(32 bit toán): yes lại trật tự, nhưng kết quả tương tự phải xảy ra, vì vậy không lại đặt hàng OP đề xuất. Dưới đây là giống nhau

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... chúng ta có thể tin tưởng họ nhận thấy sự không nhất quán của kết quả và giữ nguyên thứ tự biểu thức không?

Tin tưởng là có, nhưng mục tiêu mã hóa của OP không rõ ràng. Có nên u32_x + u32_yđóng góp? Nếu OP muốn đóng góp đó, mã phải là

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Nhưng không

uint64_t u64_z = u32_x + u32_y + u64_a;
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.