Kết quả dấu phẩy động khác nhau khi bật tối ưu hóa - lỗi trình biên dịch?


109

Đoạn mã dưới đây hoạt động trên Visual Studio 2008 có và không có tối ưu hóa. Nhưng nó chỉ hoạt động trên g ++ mà không có tối ưu hóa (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Đầu ra phải là:

4.5
4.6

Nhưng g ++ với tối ưu hóa ( O1- O3) sẽ xuất ra:

4.5
4.5

Nếu tôi thêm volatiletừ khóa trước t, nó hoạt động, vì vậy có thể có một số loại lỗi tối ưu hóa?

Thử nghiệm trên g ++ 4.1.2 và 4.4.4.

Đây là kết quả trên Ideone: http://ideone.com/Rz937

Và tùy chọn tôi thử nghiệm trên g ++ rất đơn giản:

g++ -O2 round.cpp

Kết quả thú vị hơn, ngay cả khi tôi bật /fp:fasttùy chọn trên Visual Studio 2008, kết quả vẫn chính xác.

Câu hỏi thêm:

Tôi đã tự hỏi, tôi có nên bật -ffloat-storetùy chọn luôn không?

Vì phiên bản g ++ mà tôi đã thử nghiệm được vận chuyển cùng với CentOS / Red Hat Linux 5 và CentOS / Redhat 6 .

Tôi đã biên soạn nhiều chương trình của mình trên các nền tảng này và tôi lo rằng nó sẽ gây ra các lỗi không mong muốn bên trong các chương trình của tôi. Có vẻ hơi khó khăn để điều tra tất cả mã C ++ của tôi và các thư viện đã sử dụng xem chúng có gặp sự cố như vậy không. Bất kì lời đề nghị nào?

Có ai quan tâm đến việc tại sao ngay cả khi được /fp:fastbật, Visual Studio 2008 vẫn hoạt động không? Có vẻ như Visual Studio 2008 đáng tin cậy hơn ở vấn đề này so với g ++?


51
Đối với tất cả người dùng SO mới: Đây là cách bạn đặt câu hỏi. +1
10

1
FWIW, tôi đang nhận được đầu ra chính xác với g ++ 4.5.0 bằng cách sử dụng MinGW.
Steve Blackwell

2
ideone sử dụng 4.3.4 ideone.com/b8VXg
Daniel A. White

5
Bạn nên nhớ rằng thói quen của bạn không chắc hoạt động đáng tin cậy với tất cả các loại đầu ra. Ngược lại với việc làm tròn một số nhân đôi thành một số nguyên, điều này rất dễ xảy ra bởi thực tế là không phải tất cả các số thực đều có thể được biểu diễn, vì vậy bạn có thể mong đợi nhận được nhiều lỗi như thế này.
Jakub Wieczorek

2
Đối với những người không thể tạo lại lỗi: không bỏ ghi chú các stmts gỡ lỗi đã nhận xét, chúng ảnh hưởng đến kết quả.
n. 'đại từ' m.

Câu trả lời:


91

Bộ xử lý Intel x86 sử dụng độ chính xác mở rộng 80-bit bên trong, trong khi doublethông thường là rộng 64-bit. Các mức tối ưu hóa khác nhau ảnh hưởng đến tần suất các giá trị dấu chấm động từ CPU được lưu vào bộ nhớ và do đó được làm tròn từ độ chính xác 80 bit thành độ chính xác 64 bit.

Sử dụng -ffloat-storetùy chọn gcc để nhận được kết quả dấu phẩy động giống nhau với các mức tối ưu hóa khác nhau.

Ngoài ra, hãy sử dụng long doublekiểu, thường có độ rộng 80 bit trên gcc để tránh làm tròn từ độ chính xác 80 bit thành 64 bit.

man gcc nói lên tất cả:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

Trong các trình biên dịch xây dựng x86_64 sử dụng thanh ghi SSE cho floatdoubletheo mặc định, do đó không có độ chính xác mở rộng nào được sử dụng và sự cố này không xảy ra.

gcctùy chọn trình biên dịch-mfpmath kiểm soát điều đó.


20
Tôi nghĩ đây là câu trả lời. Hằng số 4,55 được chuyển đổi thành 4,54999999999999, là biểu diễn nhị phân gần nhất trong 64 bit; nhân với 10 và làm tròn lại thành 64 bit và bạn nhận được 45,5. Nếu bạn bỏ qua bước làm tròn bằng cách giữ nó trong sổ đăng ký 80 bit, bạn sẽ nhận được 45.4999999999999.
Mark Ransom

Cảm ơn, tôi thậm chí không biết tùy chọn này. Nhưng tôi tự hỏi, tôi có nên luôn bật tùy chọn -ffloat-store không? Vì phiên bản g ++ mà tôi đã thử nghiệm được vận chuyển cùng với CentOS / Redhat 5 và CentOS / Redhat 6. Tôi đã biên dịch nhiều chương trình của mình trên các nền tảng này, tôi lo lắng về điều đó sẽ gây ra lỗi không mong muốn bên trong các chương trình của tôi.
Bear

5
@Bear, câu lệnh gỡ lỗi có thể khiến giá trị được chuyển từ một thanh ghi vào bộ nhớ.
Mark Ransom

2
@Bear, thông thường ứng dụng của bạn sẽ được hưởng lợi từ độ chính xác mở rộng, trừ khi nó hoạt động trên các giá trị cực nhỏ hoặc cực lớn khi float 64-bit dự kiến ​​sẽ thiếu hoặc tràn và tạo ra inf. Không có nguyên tắc chung nào, các bài kiểm tra đơn vị có thể cho bạn câu trả lời chắc chắn.
Maxim Egorushkin

2
@bear Theo nguyên tắc chung, nếu bạn cần kết quả hoàn toàn có thể dự đoán được và / hoặc chính xác những gì con người sẽ nhận được khi tính tổng trên giấy thì bạn nên tránh dấu phẩy động. -ffloat-store loại bỏ một nguồn khó đoán nhưng nó không phải là một viên đạn ma thuật.
plugwash

10

Kết quả đầu ra phải là: 4,5 4,6 Đó là kết quả đầu ra nếu bạn có độ chính xác vô hạn hoặc nếu bạn đang làm việc với một thiết bị sử dụng biểu diễn dấu phẩy động dựa trên thập phân chứ không phải dựa trên nhị phân. Nhưng, bạn không. Hầu hết các máy tính sử dụng tiêu chuẩn dấu chấm động IEEE nhị phân.

Như Maxim Yegorushkin đã lưu ý trong câu trả lời của mình, một phần của vấn đề là bên trong máy tính của bạn đang sử dụng biểu diễn dấu chấm động 80 bit. Tuy nhiên, đây chỉ là một phần của vấn đề. Cơ sở của vấn đề là bất kỳ số nào có dạng n.nn5 không có biểu diễn thực nhị phân chính xác. Các trường hợp góc đó luôn là số không chính xác.

Nếu bạn thực sự muốn làm tròn số của mình có thể làm tròn các trường hợp góc này một cách đáng tin cậy, bạn cần một thuật toán làm tròn giải quyết thực tế là n.n5, n.nn5 hoặc n.nnn5, v.v. (nhưng không phải n.5) luôn luôn không chính xác. Tìm trường hợp góc xác định xem một số giá trị đầu vào làm tròn lên hay xuống và trả về giá trị làm tròn lên hoặc làm tròn xuống dựa trên so sánh với trường hợp góc này. Và bạn cần phải lưu ý rằng một trình biên dịch tối ưu hóa sẽ không đặt trường hợp góc được tìm thấy đó trong một thanh ghi chính xác mở rộng.

Xem Làm thế nào để Excel làm tròn thành công các số nổi ngay cả khi chúng không chính xác? cho một thuật toán như vậy.

Hoặc bạn có thể sống với thực tế rằng các trường hợp góc đôi khi sẽ làm tròn sai.


6

Các trình biên dịch khác nhau có các cài đặt tối ưu hóa khác nhau. Một số cài đặt tối ưu hóa nhanh hơn không duy trì các quy tắc dấu phẩy động nghiêm ngặt theo IEEE 754 . Visual Studio có một thiết lập cụ thể, /fp:strict, /fp:precise, /fp:fast, nơi /fp:fastvi phạm các tiêu chuẩn về những gì có thể được thực hiện. Bạn có thể thấy rằng đây cờ là cái gì kiểm soát tối ưu hóa trong cài đặt như vậy. Bạn cũng có thể tìm thấy cài đặt tương tự trong GCC để thay đổi hành vi.

Nếu đúng như vậy thì điều duy nhất khác biệt giữa các trình biên dịch là GCC sẽ tìm kiếm hành vi dấu chấm động nhanh nhất theo mặc định trên các mức tối ưu hóa cao hơn, trong khi Visual Studio không thay đổi hành vi dấu chấm động với mức tối ưu hóa cao hơn. Vì vậy, nó có thể không nhất thiết phải là một lỗi thực sự, mà là hành vi có chủ đích của một tùy chọn mà bạn không biết là mình đang bật.


4
Có một -ffast-mathcông tắc cho GCC và nó không được bật bởi bất kỳ -Ocấp độ tối ưu hóa nào kể từ khi trích dẫn: "nó có thể dẫn đến kết quả đầu ra không chính xác cho các chương trình phụ thuộc vào việc triển khai chính xác các quy tắc / thông số kỹ thuật IEEE hoặc ISO cho các hàm toán học."
Mat

@Mat: Tôi đã thử -ffast-mathvà một số cách khác trên của tôi g++ 4.4.3và tôi vẫn không thể tái tạo sự cố.
NPE

Tốt: với -ffast-mathtôi nhận được 4.5trong cả hai trường hợp cho mức độ tối ưu hóa lớn hơn 0.
Kerrek SB

(Đính chính: Tôi nhận được 4.5với -O1-O2, nhưng không phải với -O0-O3trong GCC 4.4.3, mà với -O1,2,3trong GCC 4.6.1.)
Kerrek SB

4

Đối với những người không thể tạo lại lỗi: không bỏ ghi chú các stmts gỡ lỗi đã nhận xét, chúng ảnh hưởng đến kết quả.

Điều này ngụ ý rằng vấn đề có liên quan đến các câu lệnh gỡ lỗi. Và có vẻ như có lỗi làm tròn do tải các giá trị vào thanh ghi trong các câu lệnh đầu ra, đó là lý do tại sao những người khác nhận thấy rằng bạn có thể sửa lỗi này bằng-ffloat-store

Câu hỏi thêm:

Tôi đã tự hỏi, tôi có nên bật -ffloat-storetùy chọn luôn không?

Để trở thành thiếu nghiêm túc, phải có một lý do mà một số lập trình viên không bật -ffloat-store, nếu không lựa chọn sẽ không tồn tại (tương tự như vậy, phải có một lý do mà một số lập trình viên làm bật -ffloat-store). Tôi không khuyên bạn nên luôn bật hoặc luôn tắt nó đi. Việc bật nó sẽ ngăn chặn một số tối ưu hóa, nhưng tắt nó cho phép loại hành vi bạn đang nhận được.

Tuy nhiên, nói chung, có một số không khớp giữa số dấu phẩy động nhị phân (như máy tính sử dụng) và số dấu phẩy động thập phân (mà mọi người quen thuộc) và sự không khớp đó có thể gây ra hành vi tương tự với những gì bạn nhận được (nói rõ hơn là hành vi bạn nhận được không phải do sự không khớp này gây ra, nhưng hành vi tương tự có thể xảy ra). Vấn đề là, vì bạn đã có một số mơ hồ khi xử lý dấu phẩy động, tôi không thể nói điều đó -ffloat-storelàm cho nó tốt hơn hay tệ hơn.

Thay vào đó, bạn có thể muốn xem xét các giải pháp khác cho vấn đề bạn đang cố gắng giải quyết (rất tiếc, Koenig không chỉ ra bài báo thực tế và tôi thực sự không thể tìm thấy một nơi rõ ràng "chuẩn" cho nó, vì vậy tôi Tôi sẽ phải đưa bạn đến Google ).


Nếu bạn không làm tròn cho mục đích đầu ra, tôi có thể sẽ xem xét std::modf()(in cmath) và std::numeric_limits<double>::epsilon()(in limits). Suy nghĩ về round()chức năng ban đầu , tôi tin rằng sẽ gọn gàng hơn nếu thay thế lệnh gọi đến std::floor(d + .5)bằng lệnh gọi đến hàm này:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Tôi nghĩ rằng điều đó gợi ý sự cải thiện sau:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Một lưu ý đơn giản: std::numeric_limits<T>::epsilon()được định nghĩa là "số nhỏ nhất được thêm vào 1 tạo ra một số không bằng 1." Bạn thường cần sử dụng một epsilon tương đối (tức là, chia tỷ lệ epsilon bằng cách nào đó để tính đến thực tế là bạn đang làm việc với các số khác với "1"). Tổng số d, .5std::numeric_limits<double>::epsilon()nên được gần 1, vì vậy nhóm đó có nghĩa Ngoài ra rằng std::numeric_limits<double>::epsilon()sẽ có kích thước phù hợp với những gì chúng ta đang làm. Nếu bất cứ điều gì, std::numeric_limits<double>::epsilon()sẽ quá lớn (khi tổng của cả ba nhỏ hơn một) và có thể khiến chúng ta làm tròn một số số khi chúng ta không nên làm.


Ngày nay, bạn nên xem xét std::nearbyint().


Một "epsilon tương đối" được gọi là 1 ulp (1 đơn vị ở vị trí cuối cùng). x - nextafter(x, INFINITY)có liên quan đến 1 ulp cho x (nhưng không sử dụng điều đó; tôi chắc chắn rằng có những trường hợp góc và tôi chỉ tạo ra điều đó). Ví dụ về cppreference epsilon() có một ví dụ về điều chỉnh tỷ lệ để nhận được lỗi tương đối dựa trên ULP .
Peter Cordes

2
BTW, câu trả lời năm 2016 -ffloat-storelà: không sử dụng x87 ngay từ đầu. Sử dụng toán học SSE2 (mã nhị phân 64-bit hoặc -mfpmath=sse -msse2để tạo mã nhị phân 32-bit cũ kỹ), vì SSE / SSE2 có các mã tạm thời không có độ chính xác cao. doublevà các floatvars trong thanh ghi XMM thực sự ở định dạng IEEE 64-bit hoặc 32-bit. (Không giống như x87, nơi các thanh ghi luôn là 80-bit và lưu trữ vào bộ nhớ làm tròn thành 32 hoặc 64 bit.)
Peter Cordes

3

Câu trả lời được chấp nhận là đúng nếu bạn đang biên dịch tới mục tiêu x86 không bao gồm SSE2. Tất cả các bộ xử lý x86 hiện đại đều hỗ trợ SSE2, vì vậy nếu bạn có thể tận dụng nó, bạn nên:

-mfpmath=sse -msse2 -ffp-contract=off

Hãy phá vỡ điều này.

-mfpmath=sse -msse2. Điều này thực hiện làm tròn bằng cách sử dụng các thanh ghi SSE2, nhanh hơn nhiều so với việc lưu trữ mọi kết quả trung gian vào bộ nhớ. Lưu ý rằng đây đã là mặc định trên GCC cho x86-64. Từ wiki GCC :

Trên các bộ xử lý x86 hiện đại hơn hỗ trợ SSE2, việc chỉ định các tùy chọn trình biên dịch -mfpmath=sse -msse2đảm bảo tất cả các hoạt động float và double được thực hiện trong các thanh ghi SSE và được làm tròn chính xác. Các tùy chọn này không ảnh hưởng đến ABI và do đó nên được sử dụng bất cứ khi nào có thể để có kết quả số có thể dự đoán được.

-ffp-contract=off. Tuy nhiên, kiểm soát làm tròn là không đủ cho một trận đấu chính xác. Các lệnh FMA (hợp nhất nhân-cộng) có thể thay đổi hành vi làm tròn so với các lệnh không hợp nhất của nó, vì vậy chúng ta cần vô hiệu hóa nó. Đây là mặc định trên Clang, không phải GCC. Như được giải thích bởi câu trả lời này :

FMA chỉ có một lần làm tròn (nó có hiệu quả giữ độ chính xác vô hạn cho kết quả nhân tạm thời bên trong), trong khi ADD + MUL có hai.

Bằng cách tắt FMA, chúng tôi nhận được kết quả khớp chính xác khi gỡ lỗi và phát hành, với cái giá phải trả là hiệu suất (và độ chính xác). Chúng tôi vẫn có thể tận dụng các lợi ích hiệu suất khác của SSE và AVX.


1

Tôi đã đào sâu hơn vào vấn đề này và tôi có thể đưa ra nhiều lựa chọn hơn. Đầu tiên, các biểu diễn chính xác của 4,45 và 4,55 theo gcc trên x84_64 là như sau (với libquadmath để in độ chính xác cuối cùng):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Như Maxim đã nói ở trên, vấn đề là do kích thước 80 bit của các thanh ghi FPU.

Nhưng tại sao sự cố không bao giờ xảy ra trên Windows? trên IA-32, FPU x87 đã được định cấu hình để sử dụng độ chính xác nội bộ cho phần định trị là 53 bit (tương đương với tổng kích thước là 64 bit double:). Đối với Linux và Mac OS, độ chính xác mặc định là 64 bit đã được sử dụng (tương đương với tổng kích thước là 80 bit long double:). Vì vậy, vấn đề có thể xảy ra hoặc không, trên các nền tảng khác nhau này bằng cách thay đổi từ điều khiển của FPU (giả sử chuỗi hướng dẫn sẽ kích hoạt lỗi). Vấn đề đã được báo cáo cho gcc là lỗi 323 (đọc ít nhất là nhận xét 92!).

Để hiển thị độ chính xác phần định trị trên Windows, bạn có thể biên dịch mã này thành 32 bit với VC ++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

và trên Linux / Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Lưu ý rằng với gcc, bạn có thể đặt độ chính xác FPU -mpc32/64/80, mặc dù nó bị bỏ qua trong Cygwin. Nhưng hãy nhớ rằng nó sẽ sửa đổi kích thước của phần định trị, nhưng không phải của phần mũ, để mở ra cánh cửa cho các loại hành vi khác nhau.

Trên kiến ​​trúc x86_64, SSE được sử dụng như đã nói bởi tmandry , vì vậy sự cố sẽ không xảy ra trừ khi bạn buộc FPU x87 cũ cho tính toán FP với -mfpmath=387hoặc trừ khi bạn biên dịch ở chế độ 32 bit với -m32(bạn sẽ cần gói multilib). Tôi có thể tái tạo sự cố trên Linux bằng các tổ hợp cờ và phiên bản gcc khác nhau:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Tôi đã thử một số kết hợp trên Windows hoặc Cygwin với VC ++ / gcc / tcc nhưng lỗi không bao giờ xuất hiện. Tôi cho rằng chuỗi lệnh được tạo không giống nhau.

Cuối cùng, lưu ý rằng một cách kỳ lạ để ngăn chặn vấn đề này với 4.45 hoặc 4.55 sẽ là sử dụng _Decimal32/64/128, nhưng sự hỗ trợ thực sự rất khan hiếm ... Tôi đã dành rất nhiều thời gian chỉ để có thể thực hiện một printf với libdfp!


0

Cá nhân tôi đã gặp vấn đề tương tự theo cách khác - từ gcc sang VS. Trong hầu hết các trường hợp, tôi nghĩ tốt hơn là nên tránh tối ưu hóa. Lần duy nhất đáng giá là khi bạn đang xử lý các phương pháp số liên quan đến mảng lớn dữ liệu dấu phẩy động. Ngay cả sau khi tháo rời, tôi thường bị choáng ngợp bởi các lựa chọn của trình biên dịch. Thông thường, việc sử dụng bản chất của trình biên dịch hoặc chỉ cần tự viết assembly sẽ dễ dàng hơn.

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.