Tối ưu hóa dấu phẩy động này có được phép không?


90

Tôi đã cố gắng kiểm tra nơi float mất khả năng biểu diễn chính xác các số nguyên lớn. Vì vậy, tôi đã viết đoạn mã nhỏ này:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Mã này dường như hoạt động với tất cả các trình biên dịch, ngoại trừ tiếng kêu. Clang tạo ra một vòng lặp vô hạn đơn giản.Chốt thần .

Điều này có được phép không? Nếu có, đó có phải là vấn đề QoI không?


@geza Tôi muốn nghe con số kết quả!
nada,

5
gccthực hiện tối ưu hóa vòng lặp vô hạn tương tự nếu bạn biên dịch -Ofastthay thế, vì vậy đó là một tối ưu hóa được gcccoi là không an toàn, nhưng nó có thể làm được.
12345ieee

3
g ++ cũng tạo ra một vòng lặp vô hạn, nhưng nó không tối ưu hóa công việc từ bên trong nó. Bạn có thể thấy nó hoạt động ucomiss xmm0,xmm0để so sánh (float)ivới chính nó. Đó là manh mối đầu tiên của bạn rằng nguồn C ++ của bạn không có nghĩa là những gì bạn nghĩ. Bạn có tuyên bố rằng bạn có vòng lặp này để in / trả lại 16777216không? Trình biên dịch / phiên bản / tùy chọn đó với gì? Bởi vì đó sẽ là một lỗi trình biên dịch. gcc tối ưu hóa chính xác mã của bạn thành jnpnhánh vòng lặp ( godbolt.org/z/XJYWeu ): tiếp tục lặp lại miễn là các toán hạng != không phải là NaN.
Peter Cordes

4
Cụ thể, đó là -ffast-mathtùy chọn được kích hoạt ngầm -Ofastcho phép GCC áp dụng các tối ưu hóa dấu phẩy động không an toàn và do đó tạo ra mã giống như Clang. MSVC hoạt động theo cùng một cách: không có /fp:fast, nó tạo ra một loạt mã dẫn đến một vòng lặp vô hạn; với /fp:fast, nó phát ra một jmplệnh duy nhất . Tôi giả định rằng nếu không bật rõ ràng các tối ưu hóa FP không an toàn, các trình biên dịch này sẽ bị treo với các yêu cầu IEEE 754 liên quan đến giá trị NaN. Thật thú vị là Clang không thực sự. Máy phân tích tĩnh của nó tốt hơn. @ 12345ieee
Cody Grey

1
@geza: Nếu mã đã thực hiện những gì bạn dự định, kiểm tra xem khi giá trị toán học của (float) ikhác với giá trị toán học của i, thì kết quả (giá trị được trả về trong returncâu lệnh) sẽ là 16,777,217, không phải 16,777,216.
Eric Postpischil

Câu trả lời:


49

Như @Angew đã chỉ ra , !=nhà điều hành cần cùng một loại ở cả hai bên. (float)i != ikết quả là thúc đẩy RHS nổi, vì vậy chúng tôi có (float)i != (float)i.


g ++ cũng tạo ra một vòng lặp vô hạn, nhưng nó không tối ưu hóa công việc từ bên trong nó. Bạn có thể thấy nó chuyển đổi int-> float với cvtsi2ssvà does ucomiss xmm0,xmm0để so sánh (float)ivới chính nó. (Đó là manh mối đầu tiên của bạn rằng nguồn C ++ của bạn không có nghĩa là bạn nghĩ nó đã làm như câu trả lời của @ Angew giải thích.)

x != xchỉ đúng khi nó "không có thứ tự" vì xlà NaN. ( INFINITYso sánh bằng chính nó trong toán học IEEE, nhưng NaN thì không. NAN == NANlà false, NAN != NANlà true).

gcc7.4 trở lên tối ưu hóa chính xác mã của bạn thành jnpnhánh vòng lặp ( https://godbolt.org/z/fyOhW1 ): tiếp tục lặp lại miễn là các toán hạng x != x không phải là NaN. (gcc8 trở lên cũng kiểm tra jeđể thoát khỏi vòng lặp, không thể tối ưu hóa dựa trên thực tế là nó sẽ luôn đúng với bất kỳ đầu vào không phải NaN nào). x86 FP so sánh PF đã đặt trên không có thứ tự.


Và BTW, điều đó có nghĩa là tối ưu hóa của clang cũng an toàn : nó chỉ cần CSE (float)i != (implicit conversion to float)igiống nhau và chứng minh rằng i -> floatkhông bao giờ là NaN cho phạm vi có thể int.

(Mặc dù cho rằng vòng lặp này sẽ chạm vào UB tràn có dấu, nó được phép phát ra theo nghĩa đen bất kỳ asm nào mà nó muốn, bao gồm lệnh ud2bất hợp pháp hoặc một vòng lặp vô hạn trống bất kể thân vòng thực sự là gì.) Nhưng bỏ qua UB tràn có dấu. , tối ưu hóa này vẫn hợp pháp 100%.


GCC không thể tối ưu hóa phần nội dung vòng lặp ngay cả -fwrapvkhi làm cho phần tràn số nguyên có dấu được xác định rõ ràng (dưới dạng bao bọc bổ sung của 2). https://godbolt.org/z/t9A8t_

Ngay cả việc bật -fno-trapping-mathcũng không giúp được gì. ( Thật không may , mặc định của GCC là cho phép
-ftrapping-mathmặc dù việc triển khai nó bị hỏng / lỗi .) Int-> chuyển đổi float có thể gây ra ngoại lệ không chính xác FP (đối với các số quá lớn để được thể hiện chính xác), vì vậy, với các ngoại lệ có thể được hiển thị, bạn không nên tối ưu hóa phần thân vòng lặp. (Vì chuyển đổi 16777217thành float có thể có tác dụng phụ có thể quan sát được nếu ngoại lệ không chính xác được hiển thị.)

Nhưng với -O3 -fwrapv -fno-trapping-math, việc tối ưu hóa bị bỏ lỡ 100% là không biên dịch nó thành một vòng lặp vô hạn trống. Nếu không #pragma STDC FENV_ACCESS ON, trạng thái của các cờ dính ghi lại các ngoại lệ FP bị che không phải là một tác dụng phụ có thể quan sát được của mã. Không int-> floatchuyển đổi có thể dẫn đến NaN, vì vậy x != xkhông thể đúng.


Tất cả các trình biên dịch này đều đang tối ưu hóa cho việc triển khai C ++ sử dụng IEEE 754 đơn chính xác (binary32) floatvà 32 bit int.

Các bugfixed(int)(float)i != i vòng lặp sẽ có UB trên C ++ triển khai với hẹp 16-bit intvà / hoặc rộng hơn float, bởi vì bạn muốn nhấn ký-integer overflow UB trước khi đến số nguyên đầu tiên đó là không chính xác biểu diễn bằng float.

Nhưng UB dưới một tập hợp các lựa chọn do triển khai xác định khác không có bất kỳ hậu quả tiêu cực nào khi biên dịch cho một triển khai như gcc hoặc clang với x86-64 System V ABI.


BTW, bạn có thể tính toán tĩnh kết quả của vòng lặp này từ FLT_RADIXFLT_MANT_DIG, được định nghĩa trong <climits>. Hoặc ít nhất bạn có thể về lý thuyết, nếu floatthực sự phù hợp với mô hình của IEEE float hơn là một số loại biểu diễn số thực khác như Posit / unum.

Tôi không chắc tiêu chuẩn ISO C ++ floatxác định được bao nhiêu về hành vi và liệu một định dạng không dựa trên các trường mũ và nghĩa có độ rộng cố định có tuân thủ tiêu chuẩn hay không.


Trong các bình luận:

@geza Tôi muốn nghe con số kết quả!

@nada: đó là 16777216

Bạn có tuyên bố rằng bạn có vòng lặp này để in / trả lại 16777216không?

Cập nhật: vì bình luận đó đã bị xóa, tôi nghĩ là không. Có thể OP chỉ trích dẫn floattrước số nguyên đầu tiên không thể được biểu diễn chính xác dưới dạng 32-bit float. https://en.wikipedia.org/wiki/Single-pre khít_floating-point_format#Pre precision_limits_on_integer_values ​​tức là những gì họ hy vọng sẽ xác minh với mã lỗi này.

Tất nhiên, phiên bản được sửa lỗi sẽ in ra 16777217, số nguyên đầu tiên không thể đại diện chính xác thay vì giá trị trước đó.

(Tất cả các giá trị float cao hơn đều là số nguyên chính xác, nhưng chúng là bội số của 2, sau đó 4, rồi 8, v.v. cho các giá trị lũy thừa cao hơn ý nghĩa và độ rộng. Nhiều giá trị số nguyên cao hơn có thể được biểu diễn, nhưng 1 đơn vị ở vị trí cuối cùng (của ý nghĩa và) lớn hơn 1 nên chúng không phải là số nguyên liền kề. Số hữu hạn lớn nhất floatchỉ dưới 2 ^ 128, quá lớn so với số chẵn int64_t.)

Nếu bất kỳ trình biên dịch nào thoát khỏi vòng lặp ban đầu và in ra, đó sẽ là lỗi trình biên dịch.


3
@SombreroChicken: không, tôi học điện tử đầu tiên (từ một số sách giáo khoa mà bố tôi đã nói dối; ông ấy là một giáo sư vật lý), sau đó là logic kỹ thuật số và sau đó là CPU / phần mềm. : P Vì vậy, tôi luôn thích hiểu mọi thứ từ đầu, hoặc nếu tôi bắt đầu với cấp độ cao hơn thì tôi muốn học ít nhất một điều gì đó về cấp độ bên dưới ảnh hưởng đến cách / tại sao mọi thứ hoạt động ở cấp độ của tôi. đang nghĩ về. (.. ví dụ như cách thức hoạt động asm và làm thế nào để tối ưu hóa nó bị ảnh hưởng bởi hạn chế thiết kế CPU / CPU kiến trúc thứ nào lần lượt xuất phát từ vật lý + toán học)
Peter Cordes

1
GCC có thể không thể tối ưu hóa ngay cả với frapw, nhưng tôi chắc chắn rằng GCC 10 -ffinite-loopsđược thiết kế cho những tình huống như thế này.
MCCCS

64

Lưu ý rằng toán tử tích hợp !=yêu cầu toán hạng của nó phải cùng loại và sẽ đạt được điều đó bằng cách sử dụng các khuyến mại và chuyển đổi nếu cần. Nói cách khác, điều kiện của bạn tương đương với:

(float)i != (float)i

Điều đó sẽ không bao giờ thất bại, và vì vậy mã cuối cùng sẽ tràn i, tạo cho chương trình của bạn Hành vi không xác định. Do đó, mọi hành vi đều có thể xảy ra.

Để kiểm tra chính xác những gì bạn muốn kiểm tra, bạn nên chuyển kết quả trở lại int:

if ((int)(float)i != i)

8
@ Džuris Đó là UB. Có không có một kết quả nhất định. Trình biên dịch có thể nhận ra rằng nó chỉ có thể kết thúc bằng UB và quyết định loại bỏ hoàn toàn vòng lặp.
Vụ kiện của Fund Monica vào

4
ý bạn là @opa static_cast<int>(static_cast<float>(i))? reinterpret_castlà UB rõ ràng có
Caleth

6
@NicHartley: Bạn đang nói (int)(float)i != ilà UB? Làm thế nào để bạn kết luận điều đó? Có, nó phụ thuộc vào các thuộc tính được xác định triển khai (vì floatkhông bắt buộc phải là IEEE754 binary32), nhưng trên bất kỳ triển khai nhất định nào, nó được xác định rõ ràng trừ khi floatcó thể đại diện chính xác tất cả các intgiá trị dương để chúng ta nhận được tràn UB có dấu. ( en.cppreference.com/w/cpp/types/climits định nghĩa FLT_RADIXFLT_MANT_DIGxác định điều này). Trong in ấn nói chung mọi thứ thực hiện xác định, giống như std::cout << sizeof(int)không phải là UB ...
Peter Cordes

2
@Caleth: reinterpret_cast<int>(float)không chính xác là UB, nó chỉ là một lỗi cú pháp / không hợp lệ. Sẽ thật tuyệt nếu cú ​​pháp đó cho phép kiểu-punning của float intthay thế cho memcpy(được xác định rõ ràng), nhưng reinterpret_cast<>chỉ hoạt động trên các kiểu con trỏ, tôi nghĩ vậy.
Peter Cordes

2
@Peter Chỉ dành cho NaN, x != xlà sự thật. Xem trực tiếp trên coliru . Trong C cũng vậy.
Deduplicator
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.