Câu hỏi Tại sao thay đổi 0.1f thành 0 làm chậm hiệu suất gấp 10 lần?


Tại sao mã bit này,

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0.1f; // <--
        y[i] = y[i] - 0.1f; // <--
    }
}

chạy nhanh hơn 10 lần so với bit sau (giống hệt nhau trừ khi được ghi chú)?

const float x[16] = {  1.1,   1.2,   1.3,     1.4,   1.5,   1.6,   1.7,   1.8,
                       1.9,   2.0,   2.1,     2.2,   2.3,   2.4,   2.5,   2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
                     1.923, 2.034, 2.145,   2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
    y[i] = x[i];
}

for (int j = 0; j < 9000000; j++)
{
    for (int i = 0; i < 16; i++)
    {
        y[i] *= x[i];
        y[i] /= z[i];
        y[i] = y[i] + 0; // <--
        y[i] = y[i] - 0; // <--
    }
}

khi biên dịch với Visual Studio 2010 SP1. (Tôi chưa thử nghiệm với các trình biên dịch khác.)


1360
2018-02-16 15:58


gốc


0 là một số nguyên theo nghĩa đen, vì vậy có thể nó phải được chuyển đổi thành float trong thời gian chạy. - Zyx 2000
Bạn đo lường sự khác biệt như thế nào? Và bạn đã sử dụng các tùy chọn nào khi biên dịch? - James Kanze
Tại sao trình biên dịch lại không làm giảm +/- 0 trong trường hợp này?!? - Michael Dorgan
@ Zyx2000 Trình biên dịch không phải là bất cứ nơi nào gần đó ngu ngốc. Tháo một ví dụ tầm thường trong LINQPad cho thấy rằng nó phun ra cùng một mã cho dù bạn sử dụng 0, 0f, 0d, hoặc thậm chí (int)0 trong một bối cảnh mà double là cần thiết. - millimoose
mức tối ưu hóa là gì? - Otto Allmendinger


Các câu trả lời:


Chào mừng bạn đến với thế giới của điểm nổi không chuẩn hóa! Họ có thể tàn phá hiệu suất !!!

Các số không bình thường (hoặc bất thường) là loại hack để có được một số giá trị phụ rất gần bằng không trong biểu diễn dấu phẩy động. Các hoạt động trên điểm nổi không chuẩn hóa có thể là hàng chục đến hàng trăm lần chậm hơn so với điểm nổi chuẩn hóa. Điều này là do nhiều bộ xử lý không thể xử lý chúng trực tiếp và phải bẫy và giải quyết chúng bằng cách sử dụng microcode.

Nếu bạn in ra các con số sau 10.000 lần lặp lại, bạn sẽ thấy rằng chúng đã hội tụ với các giá trị khác nhau tùy thuộc vào việc liệu 0 hoặc là 0.1 Được sử dụng.

Đây là mã thử nghiệm được biên dịch trên x64:

int main() {

    double start = omp_get_wtime();

    const float x[16]={1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0,2.1,2.2,2.3,2.4,2.5,2.6};
    const float z[16]={1.123,1.234,1.345,156.467,1.578,1.689,1.790,1.812,1.923,2.034,2.145,2.256,2.367,2.478,2.589,2.690};
    float y[16];
    for(int i=0;i<16;i++)
    {
        y[i]=x[i];
    }
    for(int j=0;j<9000000;j++)
    {
        for(int i=0;i<16;i++)
        {
            y[i]*=x[i];
            y[i]/=z[i];
#ifdef FLOATING
            y[i]=y[i]+0.1f;
            y[i]=y[i]-0.1f;
#else
            y[i]=y[i]+0;
            y[i]=y[i]-0;
#endif

            if (j > 10000)
                cout << y[i] << "  ";
        }
        if (j > 10000)
            cout << endl;
    }

    double end = omp_get_wtime();
    cout << end - start << endl;

    system("pause");
    return 0;
}

Đầu ra:

#define FLOATING
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007
1.78814e-007  1.3411e-007  1.04308e-007  0  7.45058e-008  6.70552e-008  6.70552e-008  5.58794e-007  3.05474e-007  2.16067e-007  1.71363e-007  1.49012e-007  1.2666e-007  1.11759e-007  1.04308e-007  1.04308e-007

//#define FLOATING
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.46842e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044
6.30584e-044  3.92364e-044  3.08286e-044  0  1.82169e-044  1.54143e-044  2.10195e-044  2.45208e-029  7.56701e-044  4.06377e-044  3.92364e-044  3.22299e-044  3.08286e-044  2.66247e-044  2.66247e-044  2.24208e-044

Lưu ý làm thế nào trong lần chạy thứ hai các con số rất gần bằng không.

Các số không chuẩn hóa thường hiếm và do đó hầu hết các bộ xử lý không cố gắng xử lý chúng một cách hiệu quả.


Để chứng minh rằng điều này có mọi thứ liên quan đến các số không chuẩn hóa, nếu chúng ta denormals tuôn ra bằng không bằng cách thêm phần này vào đầu mã:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

Sau đó, phiên bản với 0 không còn chậm hơn 10x và thực sự trở nên nhanh hơn. (Điều này yêu cầu mã phải được biên dịch với SSE được kích hoạt.)

Điều này có nghĩa là thay vì sử dụng các giá trị gần như bằng không chính xác thấp hơn này, chúng tôi chỉ làm tròn thành 0.

Thời gian: Core i7 920 @ 3,5 GHz:

//  Don't flush denormals to zero.
0.1f: 0.564067
0   : 26.7669

//  Flush denormals to zero.
0.1f: 0.587117
0   : 0.341406

Cuối cùng, điều này thực sự không liên quan đến việc đó là một số nguyên hay dấu phảy động. Các 0 hoặc là 0.1f được chuyển đổi / lưu trữ thành một thanh ghi bên ngoài cả hai vòng lặp. Vì vậy, điều đó không ảnh hưởng đến hiệu suất.


1470
2018-02-16 16:20



Tôi vẫn còn tìm thấy nó một chút lạ rằng "+ 0" không hoàn toàn được tối ưu hóa bởi trình biên dịch theo mặc định. Điều này có xảy ra nếu anh ta đã đặt "+ 0.0f"? - s73v3r
@ s73v3r Đó là một câu hỏi rất hay. Bây giờ tôi nhìn vào hội đồng, thậm chí không + 0.0f được tối ưu hóa. Nếu tôi phải đoán, nó có thể là + 0.0f sẽ có tác dụng phụ nếu y[i] xảy ra là một tín hiệu NaN hoặc một cái gì đó ... tôi có thể là sai mặc dù. - Mysticial
Đôi khi vẫn sẽ gặp vấn đề tương tự trong nhiều trường hợp, chỉ ở một mức độ khác nhau về số lượng. Flush-to-zero là tốt cho các ứng dụng âm thanh (và những người khác, nơi bạn có thể đủ khả năng để mất 1e-38 ở đây và ở đó), nhưng tôi tin rằng không áp dụng cho x87. Nếu không có FTZ, việc sửa lỗi thông thường cho các ứng dụng âm thanh là tiêm một biên độ rất thấp (không nghe được) DC hoặc tín hiệu sóng vuông đến các số jitter tránh xa sự biến dạng. - Russell Borogove
@Isaac bởi vì khi y [i] nhỏ hơn đáng kể so với 0,1, kết quả là mất chính xác vì chữ số quan trọng nhất trong số đó trở nên cao hơn. - Dan Neely
@ s73v3r: + 0.f không thể được tối ưu hóa bởi vì dấu phẩy động có 0 âm và kết quả của việc thêm + 0.f vào -.0f là + 0.f. Vì vậy, việc thêm 0.f không phải là một hoạt động nhận dạng và không thể được tối ưu hóa. - Eric Postpischil


Sử dụng gcc và áp dụng sự khác biệt cho assembly được tạo ra chỉ có sự khác biệt này:

73c68,69
<   movss   LCPI1_0(%rip), %xmm1
---
>   movabsq $0, %rcx
>   cvtsi2ssq   %rcx, %xmm1
81d76
<   subss   %xmm1, %xmm0

Các cvtsi2ssq một thực tế chậm hơn 10 lần.

Rõ ràng, float phiên bản sử dụng XMM đăng ký được tải từ bộ nhớ, trong khi int phiên bản chuyển đổi một int giá trị 0 đến float sử dụng cvtsi2ssq hướng dẫn, mất rất nhiều thời gian. Đi qua -O3 để gcc không giúp được gì. (gcc phiên bản 4.2.1.)

(Sử dụng double thay vì float không quan trọng, ngoại trừ việc nó thay đổi cvtsi2ssq thành một cvtsi2sdq.)

Cập nhật 

Một số thử nghiệm bổ sung cho thấy rằng nó không nhất thiết phải là cvtsi2ssq chỉ dẫn. Sau khi bị loại bỏ (sử dụng int ai=0;float a=ai; và sử dụng a thay vì 0), sự khác biệt về tốc độ vẫn còn. Vì vậy, @ Mysticial là đúng, các phao không chuẩn hóa tạo nên sự khác biệt. Điều này có thể được nhìn thấy bằng cách kiểm tra các giá trị giữa 0 và 0.1f. Điểm ngoặt trong đoạn mã trên là khoảng 0.00000000000000000000000000000001, khi các vòng lặp đột nhiên mất gấp 10 lần.

Cập nhật << 1 

Một hình dung nhỏ về hiện tượng thú vị này:

  • Cột 1: một phao, chia cho 2 cho mỗi lần lặp
  • Cột 2: biểu diễn nhị phân của phao này
  • Cột 3: thời gian thực hiện để tính tổng phao này 1e7 lần

Bạn có thể thấy rõ số mũ (9 bit cuối cùng) thay đổi thành giá trị thấp nhất của nó, khi bộ chuẩn hóa. Tại thời điểm đó, phép cộng đơn giản sẽ chậm hơn 20 lần.

0.000000000000000000000000000000000100000004670110: 10111100001101110010000011100000 45 ms
0.000000000000000000000000000000000050000002335055: 10111100001101110010000101100000 43 ms
0.000000000000000000000000000000000025000001167528: 10111100001101110010000001100000 43 ms
0.000000000000000000000000000000000012500000583764: 10111100001101110010000110100000 42 ms
0.000000000000000000000000000000000006250000291882: 10111100001101110010000010100000 48 ms
0.000000000000000000000000000000000003125000145941: 10111100001101110010000100100000 43 ms
0.000000000000000000000000000000000001562500072970: 10111100001101110010000000100000 42 ms
0.000000000000000000000000000000000000781250036485: 10111100001101110010000111000000 42 ms
0.000000000000000000000000000000000000390625018243: 10111100001101110010000011000000 42 ms
0.000000000000000000000000000000000000195312509121: 10111100001101110010000101000000 43 ms
0.000000000000000000000000000000000000097656254561: 10111100001101110010000001000000 42 ms
0.000000000000000000000000000000000000048828127280: 10111100001101110010000110000000 44 ms
0.000000000000000000000000000000000000024414063640: 10111100001101110010000010000000 42 ms
0.000000000000000000000000000000000000012207031820: 10111100001101110010000100000000 42 ms
0.000000000000000000000000000000000000006103515209: 01111000011011100100001000000000 789 ms
0.000000000000000000000000000000000000003051757605: 11110000110111001000010000000000 788 ms
0.000000000000000000000000000000000000001525879503: 00010001101110010000100000000000 788 ms
0.000000000000000000000000000000000000000762939751: 00100011011100100001000000000000 795 ms
0.000000000000000000000000000000000000000381469876: 01000110111001000010000000000000 896 ms
0.000000000000000000000000000000000000000190734938: 10001101110010000100000000000000 813 ms
0.000000000000000000000000000000000000000095366768: 00011011100100001000000000000000 798 ms
0.000000000000000000000000000000000000000047683384: 00110111001000010000000000000000 791 ms
0.000000000000000000000000000000000000000023841692: 01101110010000100000000000000000 802 ms
0.000000000000000000000000000000000000000011920846: 11011100100001000000000000000000 809 ms
0.000000000000000000000000000000000000000005961124: 01111001000010000000000000000000 795 ms
0.000000000000000000000000000000000000000002980562: 11110010000100000000000000000000 835 ms
0.000000000000000000000000000000000000000001490982: 00010100001000000000000000000000 864 ms
0.000000000000000000000000000000000000000000745491: 00101000010000000000000000000000 915 ms
0.000000000000000000000000000000000000000000372745: 01010000100000000000000000000000 918 ms
0.000000000000000000000000000000000000000000186373: 10100001000000000000000000000000 881 ms
0.000000000000000000000000000000000000000000092486: 01000010000000000000000000000000 857 ms
0.000000000000000000000000000000000000000000046243: 10000100000000000000000000000000 861 ms
0.000000000000000000000000000000000000000000022421: 00001000000000000000000000000000 855 ms
0.000000000000000000000000000000000000000000011210: 00010000000000000000000000000000 887 ms
0.000000000000000000000000000000000000000000005605: 00100000000000000000000000000000 799 ms
0.000000000000000000000000000000000000000000002803: 01000000000000000000000000000000 828 ms
0.000000000000000000000000000000000000000000001401: 10000000000000000000000000000000 815 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 42 ms
0.000000000000000000000000000000000000000000000000: 00000000000000000000000000000000 44 ms

Một cuộc thảo luận tương đương về ARM có thể được tìm thấy trong câu hỏi Stack Overflow Điểm nổi không chuẩn hóa trong Objective-C?.


399
2018-02-16 16:19



-Okhông sửa nó, nhưng -ffast-math làm. (Tôi sử dụng tất cả thời gian, IMO các trường hợp góc mà nó gây ra rắc rối chính xác không nên bật lên trong một chương trình được thiết kế đúng anyway.) - leftaroundabout
Không có chuyển đổi ở bất kỳ mức tối ưu hóa tích cực nào với gcc-4.6. - Jed


Đó là do sử dụng dấu phẩy động không chuẩn hóa. Làm thế nào để thoát khỏi cả hai nó và hình phạt hiệu suất? Đã lùng sục Internet vì cách giết những con số bất thường, có vẻ như không có cách nào "tốt nhất" để làm điều này. Tôi đã tìm thấy ba phương pháp này có thể hoạt động tốt nhất trong các môi trường khác nhau:

  • Có thể không hoạt động trong một số môi trường GCC:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • Có thể không hoạt động trong một số môi trường Visual Studio: 1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
    
  • Xuất hiện để làm việc trong cả GCC và Visual Studio:

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
    
  • Trình biên dịch Intel có các tùy chọn để vô hiệu hóa các denorm theo mặc định trên các CPU Intel hiện đại. Thêm chi tiết tại đây

  • Trình chuyển đổi trình biên dịch. -ffast-math, -msse hoặc là -mfpmath=sse sẽ vô hiệu hóa denormals và thực hiện một vài điều khác nhanh hơn, nhưng tiếc là cũng làm rất nhiều xấp xỉ khác có thể phá vỡ mã của bạn. Kiểm tra cẩn thận! Tương đương với toán học nhanh cho trình biên dịch Visual Studio là /fp:fast nhưng tôi đã không thể xác nhận điều này cũng vô hiệu hóa các denormals hay không.1


29
2018-02-26 12:15



Điều này nghe có vẻ như một câu trả lời khá cho một câu hỏi khác nhau nhưng liên quan (Làm thế nào tôi có thể ngăn chặn tính toán số từ sản xuất các kết quả không chính xác?) Nó không trả lời câu hỏi này, mặc dù. - Ben Voigt
@BenVoigt IFTFY - vaxquis
Windows X64 đi qua một thiết lập của dòng chảy đột ngột khi nó khởi động .exe, trong khi Windows 32-bit và Linux thì không. Trên linux, gcc -ffast-math nên thiết lập dòng chảy đột ngột (nhưng tôi nghĩ không phải trên Windows). Các trình biên dịch Intel được cho là khởi tạo trong main () sao cho những sự khác biệt của hệ điều hành này không đi qua, nhưng tôi đã bị cắn và cần thiết lập nó một cách rõ ràng trong chương trình. CPU Intel bắt đầu với Sandy Bridge được cho là xử lý các subnormals phát sinh trong cộng / trừ (nhưng không chia / nhân) một cách hiệu quả, do đó, có một trường hợp để sử dụng dòng dưới dần. - tim18
Microsoft / fp: nhanh (không phải là mặc định) không làm bất kỳ điều tích cực nào vốn có trong gcc -ffast-math hoặc ICL (mặc định) / fp: nhanh. Nó giống như ICL / fp: source. Vì vậy, bạn phải thiết lập / fp: (và, trong một số trường hợp, chế độ underflow) rõ ràng nếu bạn muốn so sánh các trình biên dịch này. - tim18


Trong gcc bạn có thể kích hoạt FTZ và DAZ với điều này:

#include <xmmintrin.h>

#define FTZ 1
#define DAZ 1   

void enableFtzDaz()
{
    int mxcsr = _mm_getcsr ();

    if (FTZ) {
            mxcsr |= (1<<15) | (1<<11);
    }

    if (DAZ) {
            mxcsr |= (1<<6);
    }

    _mm_setcsr (mxcsr);
}

cũng sử dụng công tắc gcc: -msse -mfpmath = sse

(tín dụng tương ứng với Carl Hetherington [1])

[1] http://carlh.net/plugins/denormals.php


19
2017-10-02 04:40



Cũng thấy fesetround() từ fenv.h (được định nghĩa cho C99) cho một cách khác, di động hơn làm tròn (linux.die.net/man/3/fesetround) (nhưng điều này sẽ ảnh hưởng đến tất cả các hoạt động của FP, không chỉ các phân mục con) - German Garcia
Bạn có chắc bạn cần 1 << 15 và 1 << 11 cho FTZ? Tôi chỉ thấy 1 << 15 trích dẫn ở nơi khác ... - fig
@fig: 1 << 11 dành cho Mặt nạ Underflow. Thông tin thêm tại đây: softpixel.com/~cwright/programming/simd/sse.php - German Garcia
@GermanGarcia này không trả lời câu hỏi OPs; câu hỏi là "Tại sao bit này mã, chạy nhanh hơn 10 lần so với ..." - bạn nên cố gắng trả lời trước khi cung cấp cách này hoặc cung cấp điều này trong nhận xét. - vaxquis


Nhận xét của Dan Neely nên được mở rộng thành một câu trả lời:

Nó không phải là hằng số 0 0.0f không được chuẩn hóa hoặc làm chậm tốc độ, đó là các giá trị tiếp cận zero mỗi lần lặp của vòng lặp. Khi chúng đến gần hơn và gần bằng không, chúng cần độ chính xác hơn để biểu diễn và chúng trở nên không chuẩn hóa. Đây là những y[i] giá trị. (Họ tiếp cận không vì x[i]/z[i] nhỏ hơn 1.0 cho tất cả i.)

Sự khác biệt quan trọng giữa các phiên bản chậm và nhanh của mã là tuyên bố y[i] = y[i] + 0.1f;. Ngay sau khi dòng này được thực thi mỗi lần lặp của vòng lặp, độ chính xác bổ sung trong phao bị mất, và việc chuẩn hóa cần thiết để biểu thị rằng độ chính xác không còn cần thiết nữa. Sau đó, các hoạt động điểm trôi nổi trên y[i] vẫn còn nhanh vì chúng không được chuẩn hóa.

Tại sao độ chính xác thêm bị mất khi bạn thêm 0.1f? Vì các số dấu phẩy động chỉ có nhiều chữ số có nghĩa. Giả sử bạn có đủ bộ nhớ cho ba chữ số có nghĩa, sau đó 0.00001 = 1e-50.00001 + 0.1 = 0.1, ít nhất là đối với định dạng nổi mẫu này, vì nó không có chỗ để lưu trữ bit ít quan trọng nhất trong 0.10001.

Nói ngắn gọn, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f; không phải là không-op bạn có thể nghĩ nó là.

Mystical cũng nói điều này: nội dung của các phao nổi, không chỉ là mã lắp ráp.


0
2017-08-01 13:32