Tại sao GCC không tối ưu hóa a * a * a * a * a * a thành (a * a * a) * (a * a * a)?


2120

Tôi đang thực hiện một số tối ưu hóa số trên một ứng dụng khoa học. Một điều tôi nhận thấy là GCC sẽ tối ưu hóa cuộc gọi pow(a,2)bằng cách biên dịch nó a*a, nhưng cuộc gọi pow(a,6)không được tối ưu hóa và thực sự sẽ gọi chức năng thư viện pow, điều này làm chậm hiệu suất rất nhiều. (Ngược lại, Trình biên dịch Intel C ++ , có thể thực thi được icc, sẽ loại bỏ lệnh gọi thư viện pow(a,6).)

Điều tôi tò mò là khi tôi thay thế pow(a,6)bằng a*a*a*a*a*acách sử dụng GCC 4.5.1 và các tùy chọn " -O3 -lm -funroll-loops -msse4", nó sử dụng 5 mulsdhướng dẫn:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

trong khi nếu tôi viết (a*a*a)*(a*a*a), nó sẽ tạo ra

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

làm giảm số lượng hướng dẫn nhân lên 3. icccó hành vi tương tự.

Tại sao trình biên dịch không nhận ra thủ thuật tối ưu hóa này?


13
"Nhận biết pow (a, 6)" nghĩa là gì?
Varun Madiath

659
Ừm ... bạn biết rằng a a a a a a và (a a a) * (a a * a) không giống với số dấu phẩy động, phải không? Bạn sẽ phải sử dụng -funafe-math hoặc -ffast-math hoặc một cái gì đó cho điều đó.
Damon

106
Tôi đề nghị bạn đọc "Điều mà mọi nhà khoa học máy tính nên biết về số học dấu phẩy động" của David Goldberg: download.oracle.com/docs/cd/E19957-01/806-3568/. Sau đó, bạn sẽ hiểu rõ hơn về hố tar mà bạn vừa bước vào!
Phil Armstrong

189
Một câu hỏi hoàn toàn hợp lý. Cách đây 20 năm, tôi đã hỏi cùng một câu hỏi chung và bằng cách phá vỡ nút thắt đơn lẻ đó, đã giảm thời gian thực hiện mô phỏng Monte Carlo từ 21 giờ xuống còn 7 giờ. Mã trong vòng lặp bên trong đã được thực hiện 13 nghìn tỷ lần trong quy trình, nhưng nó đã mô phỏng thành một cửa sổ qua đêm. (xem câu trả lời bên dưới)

23
Có lẽ ném (a*a)*(a*a)*(a*a)vào hỗn hợp, quá. Cùng một số phép nhân, nhưng có lẽ chính xác hơn.
Rok Kralj

Câu trả lời:


2738

Bởi vì Toán học dấu phẩy động không liên kết . Cách bạn nhóm các toán hạng trong phép nhân dấu phẩy động có ảnh hưởng đến độ chính xác của câu trả lời.

Do đó, hầu hết các trình biên dịch đều rất thận trọng về việc sắp xếp lại các phép tính dấu phẩy động trừ khi chúng có thể chắc chắn rằng câu trả lời sẽ giữ nguyên hoặc trừ khi bạn nói với chúng rằng bạn không quan tâm đến độ chính xác của số. Ví dụ: các -fassociative-mathtùy chọn của gcc gcc cho phép để hoạt động điểm nổi reassociate, hoặc thậm chí các -ffast-mathtùy chọn cho phép thậm chí cân bằng tích cực hơn về tính chính xác đối với tốc độ.


10
Đúng. Với -ffast-math, nó đang thực hiện tối ưu hóa như vậy. Ý tưởng tốt! Nhưng vì mã của chúng tôi liên quan đến độ chính xác cao hơn tốc độ, tốt hơn là không nên vượt qua nó.
xis

19
IIRC C99 cho phép trình biên dịch thực hiện tối ưu hóa FP "không an toàn" như vậy, nhưng GCC (trên bất kỳ thứ gì khác ngoài x87) thực hiện một nỗ lực hợp lý theo tiêu chuẩn IEEE 754 - đó không phải là "giới hạn lỗi"; chỉ có một câu trả lời đúng .
tc.

14
Các chi tiết thực hiện powkhông có ở đây cũng không có; Câu trả lời này thậm chí không tham khảo pow.
Stephen Canon

14
@nedR: ICC mặc định cho phép liên kết lại. Nếu bạn muốn có được hành vi tuân thủ tiêu chuẩn, bạn cần thiết lập -fp-model precisevới ICC. clanggccmặc định để tái tổ chức wrt tuân thủ nghiêm ngặt.
Stephen Canon

49
@xis, nó không thực sự -fassociative-mathsẽ không chính xác; nó chỉ như vậy a*a*a*a*a*a(a*a*a)*(a*a*a)là khác nhau. Đó không phải là về độ chính xác; đó là về sự phù hợp tiêu chuẩn và kết quả lặp lại nghiêm ngặt, ví dụ như kết quả tương tự trên bất kỳ trình biên dịch nào. Số dấu phẩy động đã không chính xác. Nó hiếm khi không phù hợp để biên dịch với -fassociative-math.
Paul Draper

652

Lambdageek một cách chính xác chỉ ra rằng vì associativity không giữ cho số dấu chấm động, các "tối ưu hóa" củaa*a*a*a*a*ađể(a*a*a)*(a*a*a)có thể thay đổi giá trị. Đây là lý do tại sao nó không được phép bởi C99 (trừ khi được người dùng cho phép cụ thể, thông qua cờ trình biên dịch hoặc pragma). Nói chung, giả định là lập trình viên đã viết những gì cô ấy đã làm vì một lý do và trình biên dịch nên tôn trọng điều đó. Nếu bạn muốn(a*a*a)*(a*a*a), viết nó.

Đó có thể là một nỗi đau để viết, mặc dù; tại sao trình biên dịch không thể làm [những gì bạn cho là] đúng khi bạn sử dụng pow(a,6)? Bởi vì nó sẽ là điều sai trái để làm. Trên một nền tảng với một thư viện toán học tốt, pow(a,6)chính xác hơn đáng kể so với a*a*a*a*a*ahoặc (a*a*a)*(a*a*a). Chỉ để cung cấp một số dữ liệu, tôi đã chạy một thử nghiệm nhỏ trên Mac Pro của mình, đo lường lỗi tồi tệ nhất khi đánh giá ^ 6 cho tất cả các số nổi chính xác đơn giữa [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

Sử dụng powthay cho cây nhân sẽ giảm lỗi bị ràng buộc bởi hệ số 4 . Trình biên dịch không nên (và nói chung là không) thực hiện "tối ưu hóa" làm tăng lỗi trừ khi người dùng được cấp phép làm như vậy (ví dụ: thông qua -ffast-math).

Lưu ý rằng GCC cung cấp __builtin_powi(x,n)như là một thay thế pow( ), sẽ tạo ra một cây nhân nội tuyến. Sử dụng nếu bạn muốn đánh đổi độ chính xác để thực hiện, nhưng không muốn kích hoạt tính toán nhanh.


29
Cũng lưu ý rằng Visual C ++ cung cấp phiên bản 'nâng cao' của pow (). Bằng cách gọi _set_SSE2_enable(<flag>)với flag=1, nó sẽ sử dụng SSE2 nếu có thể. Điều này làm giảm độ chính xác một chút, nhưng cải thiện tốc độ (trong một số trường hợp). MSDN: _set_SSE2_enable ()pow ()
TkTech

18
@TkTech: Mọi độ chính xác giảm là do triển khai của Microsoft, không phải do kích thước của các thanh ghi được sử dụng. Có thể phân phối chính xác pow chỉ bằng các thanh ghi 32 bit, nếu người viết thư viện rất có động lực. Có SSE dựa trên powhiện thực mà là nhiều hơn chính xác hơn hầu hết các trường x87-based, và cũng có những hiện thực mà đánh đổi một số chính xác cho tốc độ.
Stephen Canon

9
@TkTech: Tất nhiên, tôi chỉ muốn làm rõ rằng việc giảm độ chính xác là do các lựa chọn của các nhà văn thư viện, không phải do sử dụng SSE.
Stephen Canon

7
Tôi rất muốn biết những gì bạn đã sử dụng làm "tiêu chuẩn vàng" ở đây để tính toán các lỗi tương đối - tôi thường mong đợi nó sẽ xảy ra a*a*a*a*a*a, nhưng rõ ràng không phải vậy! :)
j_random_hacker

8
@j_random_hacker: kể từ khi tôi đã so sánh kết quả chính xác đơn, cũng đủ đúp chính xác cho một tiêu chuẩn vàng - lỗi từ một một một một một một tính trong đôi là * bao la nhỏ hơn so với lỗi của bất kỳ tính toán chính xác đơn.
Stephen Canon

168

Một trường hợp tương tự: hầu hết các trình biên dịch sẽ không tối ưu hóa a + b + c + dđể (a + b) + (c + d)(đây là một tối ưu hóa từ biểu thức thứ hai có thể được pipelined tốt hơn) và đánh giá nó như được đưa ra (ví dụ như (((a + b) + c) + d)). Điều này cũng là vì các trường hợp góc:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Đầu ra này 1.000000e-05 0.000000e+00


10
Điều này không hoàn toàn giống nhau. Thay đổi thứ tự nhân / chia (không bao gồm chia cho 0) sẽ an toàn hơn so với thứ tự thay đổi của tổng / trừ. Theo ý kiến ​​khiêm tốn của tôi, trình biên dịch nên cố gắng liên kết các mults./divs. bởi vì làm như vậy sẽ giảm tổng số thao tác và bên cạnh hiệu suất đạt được cũng là mức tăng chính xác.
CoffeDeveloper

4
@DarioOO: Không an toàn hơn. Nhân và chia giống như phép cộng và phép trừ của số mũ, và việc thay đổi thứ tự có thể dễ dàng khiến cho thời gian vượt quá phạm vi có thể của số mũ. (Không hoàn toàn giống nhau, vì số mũ không bị mất độ chính xác ... nhưng việc biểu diễn vẫn còn khá hạn chế và việc sắp xếp lại có thể dẫn đến các giá trị không thể diễn tả được)
Ben Voigt

8
Tôi nghĩ rằng bạn đang thiếu một số nền tảng tính toán. Đa số và chia 2 số giới thiệu cùng một lượng lỗi. Trong khi trừ / cộng 2 số có thể gây ra lỗi lớn hơn, đặc biệt là khi 2 số có thứ tự độ lớn khác nhau, do đó an toàn hơn khi sắp xếp lại / chia so với phụ / thêm vì nó đưa ra một thay đổi nhỏ trong lỗi cuối cùng.
CoffeDeveloper

8
@DarioOO: rủi ro là khác nhau với mul / div: Sắp xếp lại hoặc tạo ra một sự thay đổi không đáng kể trong kết quả cuối cùng, hoặc số mũ tràn vào một lúc nào đó (nơi mà nó không có trước đó) và kết quả là khác nhau lớn (có khả năng + inf hoặc 0).
Peter Cordes

@GameDeveloper Áp đặt mức tăng chính xác theo những cách không thể đoán trước là vô cùng khó khăn.
tò mò

80

Fortran (được thiết kế cho máy tính khoa học) có một nhà điều hành năng lượng tích hợp và theo như tôi biết, trình biên dịch Fortran thường sẽ tối ưu hóa việc nâng lên các số nguyên theo cách tương tự như những gì bạn mô tả. Thật không may, C / C ++ không có toán tử nguồn, chỉ có chức năng thư viện pow(). Điều này không ngăn các trình biên dịch thông minh xử lý powđặc biệt và tính toán nó theo cách nhanh hơn cho các trường hợp đặc biệt, nhưng có vẻ như chúng làm điều đó ít phổ biến hơn ...

Vài năm trước tôi đã cố gắng làm cho nó thuận tiện hơn để tính toán các số nguyên theo cách tối ưu, và đã đưa ra những điều sau đây. Đó là C ++, không phải C, và vẫn phụ thuộc vào trình biên dịch có phần thông minh về cách tối ưu hóa / nội tuyến. Dù sao, hy vọng bạn có thể thấy nó hữu ích trong thực tế:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Làm rõ cho người tò mò: điều này không tìm ra cách tối ưu để tính toán sức mạnh, nhưng vì việc tìm ra giải pháp tối ưu là một vấn đề hoàn chỉnh NP và điều này chỉ đáng làm đối với các quyền lực nhỏ (trái ngược với việc sử dụng pow), không có lý do gì để làm phiền với các chi tiết.

Sau đó chỉ cần sử dụng nó như power<6>(a).

Điều này giúp bạn dễ dàng nhập các quyền hạn (không cần phải đánh vần 6 agiây bằng parens) và cho phép bạn có loại tối ưu hóa này mà không cần -ffast-mathtrong trường hợp bạn có thứ gì đó phụ thuộc chính xác như tổng bù (một ví dụ trong đó thứ tự hoạt động là cần thiết) .

Có lẽ bạn cũng có thể quên rằng đây là C ++ và chỉ sử dụng nó trong chương trình C (nếu nó biên dịch với trình biên dịch C ++).

Hy vọng điều này có thể hữu ích.

BIÊN TẬP:

Đây là những gì tôi nhận được từ trình biên dịch của mình:

Đối với a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Đối với (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Đối với power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

36
Tìm cây năng lượng tối ưu có thể khó, nhưng vì nó chỉ thú vị đối với các quyền lực nhỏ, nên câu trả lời rõ ràng là tính toán trước một lần (Knuth cung cấp một bảng lên tới 100) và sử dụng bảng mã hóa cứng đó (đó là những gì gcc thực hiện trong powi) .
Marc Glisse

7
Trên các bộ xử lý hiện đại, tốc độ bị giới hạn bởi độ trễ. Ví dụ, kết quả của phép nhân có thể có sẵn sau năm chu kỳ. Trong tình huống đó, việc tìm ra cách nhanh nhất để tạo ra sức mạnh có thể khó khăn hơn.
gnasher729

3
Bạn cũng có thể thử tìm cây năng lượng đưa ra giới hạn trên thấp nhất cho lỗi làm tròn tương đối hoặc lỗi làm tròn tương đối trung bình thấp nhất.
gnasher729

1
Boost cũng hỗ trợ cho việc này, ví dụ boost :: math :: pow <6> (n); Tôi nghĩ rằng nó thậm chí còn cố gắng giảm số lượng nhân bằng cách trích xuất các yếu tố phổ biến.
gast128

Lưu ý rằng cái cuối cùng tương đương với (a ** 2) ** 3
minmaxavg

62

GCC không thực sự tối ưu hóa a*a*a*a*a*ađể (a*a*a)*(a*a*a)khi a là một số nguyên. Tôi đã thử với lệnh này:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Có rất nhiều cờ gcc nhưng không có gì lạ mắt. Chúng có nghĩa là: Đọc từ stdin; sử dụng mức tối ưu hóa O2; danh sách ngôn ngữ lắp ráp đầu ra thay vì nhị phân; danh sách nên sử dụng cú pháp ngôn ngữ lắp ráp Intel; đầu vào bằng ngôn ngữ C (thông thường ngôn ngữ được suy ra từ phần mở rộng tệp đầu vào, nhưng không có phần mở rộng tệp khi đọc từ stdin); và viết vào thiết bị xuất chuẩn.

Đây là phần quan trọng của đầu ra. Tôi đã chú thích nó với một số ý kiến ​​cho biết những gì đang diễn ra trong ngôn ngữ lắp ráp:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Tôi đang sử dụng hệ thống GCC trên Linux Mint 16 Petra, một công cụ phái sinh Ubuntu. Đây là phiên bản gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Như các áp phích khác đã lưu ý, tùy chọn này là không thể trong dấu phẩy động, bởi vì số học dấu phẩy động không liên quan.


12
Điều này là hợp pháp cho phép nhân số nguyên vì tràn hai bổ sung là hành vi không xác định. Nếu có tràn, nó sẽ xảy ra ở đâu đó, bất kể hoạt động sắp xếp lại. Vì vậy, các biểu thức không có tràn đánh giá giống nhau, các biểu thức tràn là hành vi không xác định để trình biên dịch thay đổi điểm xảy ra tràn. gcc làm điều này với unsigned int, quá.
Peter Cordes

51

Bởi vì số dấu phẩy động 32 bit - chẳng hạn như 1.024 - không phải là 1.024. Trong máy tính, 1.024 là một khoảng: từ (1.024-e) đến (1.024 + e), trong đó "e" biểu thị một lỗi. Một số người không nhận ra điều này và cũng tin rằng * trong một * là viết tắt của phép nhân các số chính xác tùy ý mà không có bất kỳ lỗi nào được gắn vào các số đó. Lý do tại sao một số người không nhận ra điều này có lẽ là các tính toán toán học mà họ đã thực hiện ở các trường tiểu học: chỉ làm việc với các số lý tưởng mà không có lỗi kèm theo và tin rằng chỉ cần bỏ qua "e" trong khi thực hiện phép nhân. Họ không thấy "e" ẩn trong "float a = 1.2", "a * a * a" và các mã C tương tự.

Nếu phần lớn các lập trình viên nhận ra (và có thể thực thi) ý tưởng rằng C biểu hiện a * a * a * a * a * a không thực sự hoạt động với các số lý tưởng, thì trình biên dịch GCC sẽ MIỄN PHÍ để tối ưu hóa "a * a * a * a * a * a "thành" t = (a * a); t * t * t "đòi hỏi số lượng nhân nhỏ hơn. Nhưng thật không may, trình biên dịch GCC không biết liệu lập trình viên viết mã có nghĩ rằng "a" là một số có hoặc không có lỗi hay không. Và do đó, GCC sẽ chỉ làm những gì mã nguồn trông như thế - bởi vì đó là những gì GCC nhìn thấy bằng "mắt thường".

... khi bạn biết những gì loại lập trình bạn đang có, bạn có thể sử dụng công tắc "-ffast-math" nói với GCC rằng "Hey, GCC, tôi biết những gì tôi đang làm!". Điều này sẽ cho phép GCC chuyển đổi một * a * a * a * a * a thành một đoạn văn bản khác - nó trông khác với một * a * a * a * a * a - nhưng vẫn tính một số trong khoảng lỗi a * a * a * a * a * a. Điều này là ổn, vì bạn đã biết bạn đang làm việc với các khoảng, không phải là con số lý tưởng.


52
Số dấu phẩy động là chính xác. Họ chỉ không nhất thiết chính xác những gì bạn mong đợi. Hơn nữa, kỹ thuật với epsilon tự nó là một cách gần đúng với cách giải quyết mọi thứ trong thực tế, bởi vì lỗi dự kiến ​​thực sự có liên quan đến quy mô của lớp phủ, tức là bạn thường lên tới khoảng 1 LSB, nhưng có thể tăng lên với mọi thao tác được thực hiện nếu bạn không cẩn thận, vì vậy hãy tham khảo ý kiến ​​nhà phân tích số trước khi làm bất cứ điều gì không tầm thường với dấu phẩy động. Sử dụng một thư viện thích hợp nếu bạn có thể.
Donal Fellows

3
@DonalFellows: Tiêu chuẩn IEEE yêu cầu các phép tính dấu phẩy động mang lại kết quả khớp chính xác nhất với kết quả sẽ là gì nếu toán hạng nguồn là các giá trị chính xác, nhưng điều đó không có nghĩa là chúng thực sự đại diện cho các giá trị chính xác. Trong nhiều trường hợp, hữu ích hơn khi coi 0.1f là (1.677.722 +/- 0.5) / 16.777.216, nên được hiển thị với số chữ số thập phân ngụ ý bởi sự không chắc chắn đó, hơn là coi đó là số lượng chính xác (1.677.722 +/- 0,5) / 16,777,216 (sẽ được hiển thị thành 24 chữ số thập phân).
supercat

23
@supercat: IEEE-754 khá rõ ràng về điểm dữ liệu dấu phẩy động thể hiện các giá trị chính xác; mệnh đề 3.2 - 3.4 là các phần có liên quan. Tất nhiên, bạn có thể chọn giải thích chúng theo cách khác, giống như bạn có thể chọn giải thích int x = 3theo nghĩa xlà 3 +/- 0,5.
Stephen Canon

7
@supercat: Tôi hoàn toàn đồng ý, nhưng điều đó không có nghĩa là nó Distancekhông chính xác bằng giá trị số của nó; nó có nghĩa là giá trị số chỉ là một xấp xỉ với một số lượng vật lý được mô hình hóa.
Stephen Canon

10
Đối với phân tích số, não của bạn sẽ cảm ơn bạn nếu bạn diễn giải các số dấu phẩy động không phải là các khoảng, mà là các giá trị chính xác (điều này không chính xác với các giá trị mà bạn muốn). Ví dụ: nếu x ở đâu đó vòng 4,5 với sai số nhỏ hơn 0,1 và bạn tính (x + 1) - x, thì cách hiểu "khoảng" để lại cho bạn một khoảng từ 0,8 đến 1,2, trong khi cách hiểu "giá trị chính xác" cho biết kết quả của bạn sẽ là 1 với sai số tối đa là 2 ^ (- 50) với độ chính xác gấp đôi.
gnasher729

34

Chưa có áp phích nào đề cập đến sự co lại của các biểu thức nổi (tiêu chuẩn ISO C, 6.5p8 và 7.12.2). Nếu FP_CONTRACTpragma được đặt thành ON, trình biên dịch được phép xem xét một biểu thức, chẳng hạn a*a*a*a*a*anhư một thao tác đơn lẻ, như thể được đánh giá chính xác với một làm tròn đơn. Chẳng hạn, một trình biên dịch có thể thay thế nó bằng một hàm năng lượng bên trong vừa nhanh hơn và chính xác hơn. Điều này đặc biệt thú vị vì hành vi được điều khiển một phần bởi lập trình viên trực tiếp trong mã nguồn, trong khi các tùy chọn trình biên dịch được cung cấp bởi người dùng cuối đôi khi có thể được sử dụng không chính xác.

Trạng thái mặc định của FP_CONTRACTpragma được xác định theo triển khai, do đó trình biên dịch được phép thực hiện các tối ưu hóa đó theo mặc định. Do đó, mã di động cần tuân thủ nghiêm ngặt các quy tắc của IEEE 754 nên được đặt rõ ràng thành mã OFF.

Nếu một trình biên dịch không hỗ trợ pragma này, thì nó phải thận trọng bằng cách tránh bất kỳ tối ưu hóa nào như vậy, trong trường hợp nhà phát triển đã chọn đặt nó OFF.

GCC không hỗ trợ pragma này, nhưng với các tùy chọn mặc định, nó giả định là như vậy ON; do đó, đối với các mục tiêu có FMA phần cứng, nếu muốn ngăn chặn chuyển đổi a*b+cthành fma (a, b, c), người ta cần cung cấp một tùy chọn như -ffp-contract=off(để đặt pragma thành OFF) hoặc -std=c99(để nói với GCC tuân thủ một số Phiên bản tiêu chuẩn C, ở đây là C99, do đó, theo đoạn văn trên). Trước đây, tùy chọn thứ hai không ngăn cản chuyển đổi, có nghĩa là GCC không tuân thủ điểm này: https://gcc.gnu.org/ormszilla/show_orms.cgi?id=37845


3
Câu hỏi phổ biến lâu dài đôi khi cho thấy tuổi của họ. Câu hỏi này đã được hỏi và trả lời vào năm 2011, khi GCC có thể bị bào chữa vì không tôn trọng chính xác tiêu chuẩn C99 gần đây. Tất nhiên bây giờ là năm 2014, vì vậy, ahem GCC.
Pascal Cuoq

Tuy nhiên, bạn không nên trả lời các câu hỏi dấu phẩy động tương đối gần đây mà không có câu trả lời được chấp nhận chứ? ho stackoverflow.com/questions/23703408 ho
Pascal Cuoq

Tôi thấy nó ... làm phiền rằng gcc không thực hiện các pragma dấu phẩy động C99.
David Monniaux

1
Các pragma @DavidMonniaux theo định nghĩa là tùy chọn để thực hiện.
Tim Seguine

2
@TimSeguine Nhưng nếu một pragma không được triển khai, giá trị mặc định của nó cần phải hạn chế nhất để thực hiện. Tôi cho rằng đó là những gì David đã nghĩ về. Với GCC, điều này hiện đã được sửa cho FP_CONTRACT nếu một người sử dụng chế độ ISO C : nó vẫn không thực hiện pragma, nhưng ở chế độ ISO C, giờ đây nó giả định rằng pragma đã tắt.
vinc17

28

Như Lambdageek đã chỉ ra phép nhân float không liên quan và bạn có thể có độ chính xác thấp hơn, nhưng khi có độ chính xác tốt hơn, bạn có thể lập luận chống lại tối ưu hóa, vì bạn muốn có một ứng dụng xác định. Ví dụ: trong máy khách / máy chủ mô phỏng trò chơi, trong đó mọi máy khách phải mô phỏng cùng một thế giới mà bạn muốn tính toán dấu phẩy động có tính xác định.


3
@greggo Không, nó vẫn mang tính quyết định. Không có sự ngẫu nhiên được thêm vào trong bất kỳ ý nghĩa của từ này.
Alice

9
@ Alice Có vẻ như khá rõ ràng Bjorn ở đây đang sử dụng 'tính xác định' theo nghĩa mã cho cùng một kết quả trên các nền tảng khác nhau và các phiên bản trình biên dịch khác nhau, v.v. (các biến bên ngoài có thể nằm ngoài tầm kiểm soát của lập trình viên) ngẫu nhiên số thực tế tại thời gian chạy. Nếu bạn chỉ ra rằng đây không phải là cách sử dụng từ này, tôi sẽ không tranh luận với điều đó.
greggo

5
@greggo Ngoại trừ ngay cả trong cách giải thích của bạn về những gì anh ấy nói, nó vẫn sai; đó là toàn bộ quan điểm của IEEE 754, để cung cấp các đặc điểm giống hệt nhau cho hầu hết các hoạt động (nếu không phải tất cả) trên các nền tảng. Bây giờ, anh ta không đề cập đến các nền tảng hoặc phiên bản trình biên dịch, đây sẽ là một mối quan tâm hợp lệ nếu bạn muốn mọi thao tác trên mọi máy chủ / máy khách từ xa giống hệt nhau .... nhưng điều này không rõ ràng từ tuyên bố của anh ta. Một từ tốt hơn có thể là "tương tự đáng tin cậy" hoặc một cái gì đó.
Alice

8
@ Alice bạn đang lãng phí thời gian của mọi người, bao gồm cả của riêng bạn, bằng cách tranh luận về ngữ nghĩa. Ý nghĩa của anh rất rõ ràng.
Lanaru

11
@Lanaru Toàn bộ điểm của tiêu chuẩn ngữ nghĩa IS; ý nghĩa của ông đã được quyết định không rõ ràng.
Alice

28

Các chức năng thư viện như "pow" thường được chế tạo cẩn thận để mang lại sai số tối thiểu có thể (trong trường hợp chung). Điều này thường đạt được các hàm xấp xỉ bằng spline (theo nhận xét của Pascal, việc triển khai phổ biến nhất dường như là sử dụng thuật toán Remez )

về cơ bản các hoạt động sau đây:

pow(x,y);

có một lỗi cố hữu có độ lớn xấp xỉ bằng sai số trong bất kỳ phép nhân hoặc phép chia nào .

Trong khi các hoạt động sau đây:

float a=someValue;
float b=a*a*a*a*a*a;

có một lỗi cố hữu lớn hơn 5 lần lỗi của một phép nhân hoặc phép chia đơn (vì bạn đang kết hợp 5 phép nhân).

Trình biên dịch nên thực sự cẩn thận với loại tối ưu hóa mà nó đang thực hiện:

  1. nếu tối ưu hóa pow(a,6)để a*a*a*a*a*acó thể cải thiện hiệu suất, nhưng làm giảm đáng kể độ chính xác cho số dấu chấm động.
  2. nếu tối ưu hóa a*a*a*a*a*a để pow(a,6)nó thực sự có thể làm giảm độ chính xác vì "a" là một số giá trị đặc biệt cho phép nhân mà không có lỗi (một sức mạnh của 2 hoặc một số số nguyên nhỏ)
  3. nếu tối ưu hóa pow(a,6)đến (a*a*a)*(a*a*a)hoặc (a*a)*(a*a)*(a*a)vẫn có thể mất độ chính xác so với powchức năng.

Nói chung, bạn biết rằng đối với các giá trị dấu phẩy động tùy ý, "pow" có độ chính xác tốt hơn bất kỳ chức năng nào bạn có thể viết, nhưng trong một số trường hợp đặc biệt, phép nhân có thể có độ chính xác và hiệu suất tốt hơn, tùy thuộc vào nhà phát triển chọn cách nào phù hợp hơn, cuối cùng bình luận mã để không ai khác sẽ "tối ưu hóa" mã đó.

Điều duy nhất có ý nghĩa (ý kiến ​​cá nhân và rõ ràng là một lựa chọn trong GCC không có bất kỳ cờ tối ưu hóa hoặc trình biên dịch cụ thể nào) để tối ưu hóa nên thay thế "pow (a, 2)" bằng "a * a". Đó sẽ là điều duy nhất mà một nhà cung cấp trình biên dịch nên làm.


7
downvoters nên nhận ra rằng câu trả lời này là hoàn toàn tốt. Tôi có thể trích dẫn hàng tá nguồn và tài liệu để hỗ trợ câu trả lời của mình và tôi có lẽ liên quan nhiều hơn đến độ chính xác của dấu phẩy động so với bất kỳ hướng dẫn nào. Hoàn toàn hợp lý khi StackOverflow thêm thông tin còn thiếu mà các câu trả lời khác không bao gồm, vì vậy hãy lịch sự và giải thích lý do của bạn.
Nhà phát triển Coffe

1
Dường như với tôi rằng câu trả lời của Stephen Canon bao gồm những gì bạn nói. Bạn dường như nhấn mạnh rằng libms được triển khai bằng spline: chúng thường sử dụng giảm đối số (tùy thuộc vào hàm được thực hiện) cộng với một đa thức duy nhất các hệ số đã đạt được bằng các biến thể tinh vi hơn hoặc ít hơn của thuật toán Remez. Độ mượt tại các điểm giao nhau không được coi là mục tiêu đáng theo đuổi đối với các hàm libm (nếu chúng kết thúc đủ chính xác, dù sao chúng cũng tự động khá trơn tru bất kể có bao nhiêu phần miền được chia thành).
Pascal Cuoq

Nửa sau câu trả lời của bạn hoàn toàn bỏ lỡ điểm mà trình biên dịch được cho là tạo ra mã thực hiện những gì mã nguồn nói, theo giai đoạn. Ngoài ra, bạn sử dụng từ chính xác, có nghĩa là chính xác.
Pascal Cuoq

Cảm ơn sự đóng góp của bạn, tôi đã sửa một chút câu trả lời, một cái gì đó mới vẫn còn hiện diện trong 2 dòng cuối ^^
CoffeDeveloper

27

Tôi sẽ không mong đợi trường hợp này sẽ được tối ưu hóa cả. Không thể rất thường xuyên khi một biểu thức có chứa các biểu thức con có thể được nhóm lại để loại bỏ toàn bộ hoạt động. Tôi hy vọng các nhà văn trình biên dịch sẽ đầu tư thời gian của họ vào các lĩnh vực có nhiều khả năng dẫn đến các cải tiến đáng chú ý, thay vì bao gồm một trường hợp cạnh hiếm khi gặp phải.

Tôi đã rất ngạc nhiên khi biết được từ các câu trả lời khác rằng biểu thức này thực sự có thể được tối ưu hóa với các trình chuyển đổi trình biên dịch phù hợp. Tối ưu hóa là không đáng kể, hoặc đó là một trường hợp cạnh của tối ưu hóa phổ biến hơn nhiều, hoặc các trình soạn thảo trình biên dịch cực kỳ kỹ lưỡng.

Không có gì sai khi cung cấp gợi ý cho trình biên dịch như bạn đã làm ở đây. Đó là một phần bình thường và được mong đợi của quá trình tối ưu hóa vi mô để sắp xếp lại các tuyên bố và biểu thức để xem chúng sẽ mang lại sự khác biệt nào.

Mặc dù trình biên dịch có thể được chứng minh bằng cách xem xét hai biểu thức để cung cấp kết quả không nhất quán (không có các công tắc phù hợp), nhưng bạn không cần phải bị ràng buộc bởi hạn chế đó. Sự khác biệt sẽ cực kỳ nhỏ - đến mức nếu sự khác biệt quan trọng với bạn, bạn không nên sử dụng số học dấu phẩy động tiêu chuẩn ở vị trí đầu tiên.


17
Theo ghi nhận của một người bình luận khác, điều này là không đúng sự thật đến mức vô lý; sự khác biệt có thể bằng một nửa đến 10% chi phí và nếu chạy trong một vòng lặp chặt chẽ, điều đó sẽ chuyển thành nhiều hướng dẫn lãng phí để có được mức độ chính xác bổ sung không đáng kể. Nói rằng bạn không nên sử dụng FP tiêu chuẩn khi bạn đang thực hiện một ca khúc monte giống như nói rằng bạn nên luôn luôn sử dụng máy bay để đi khắp đất nước; nó bỏ qua nhiều yếu tố bên ngoài. Cuối cùng, đây KHÔNG phải là một tối ưu hóa hiếm gặp; phân tích mã chết và giảm / tái cấu trúc mã là rất phổ biến.
Alice

21

Đã có một vài câu trả lời hay cho câu hỏi này, nhưng để hoàn thiện tôi muốn chỉ ra rằng phần áp dụng của tiêu chuẩn C là 5.1.2.2.3 / 15 (giống như mục 1.9 / 9 trong phần Tiêu chuẩn C ++ 11). Phần này nói rằng các nhà khai thác chỉ có thể được tập hợp lại nếu họ thực sự liên kết hoặc giao hoán.


12

gcc thực sự có thể thực hiện tối ưu hóa này, ngay cả đối với các số dấu phẩy động. Ví dụ,

double foo(double a) {
  return a*a*a*a*a*a;
}

trở thành

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

với -O -funsafe-math-optimizations . Tuy nhiên, việc sắp xếp lại này vi phạm IEEE-754, do đó, nó yêu cầu cờ.

Các số nguyên đã ký, như Peter Cordes đã chỉ ra trong một nhận xét, có thể thực hiện tối ưu hóa này mà không cần -funsafe-math-optimizationsgiữ chính xác khi không có tràn và nếu có tràn bạn sẽ nhận được hành vi không xác định. Vì vậy, bạn nhận được

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

chỉ với -O. Đối với các số nguyên không dấu, điều đó thậm chí còn dễ dàng hơn vì chúng hoạt động với quyền hạn mod 2 và do đó có thể được sắp xếp lại một cách tự do ngay cả khi đối mặt với tràn.


1
Godbolt liên kết với double, int và unsign . gcc và clang đều tối ưu hóa cả ba cách giống nhau (với -ffast-math)
Peter Cordes

@PeterCordes Cảm ơn!
Charles
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.