Tại sao thay đổi 0,1f thành 0 làm chậm hiệu suất xuống 10 lần?


1527

Tại sao bit này của mã,

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 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. Mức tối ưu hóa là -02với sse2kích hoạt. Tôi đã không thử nghiệm với các trình biên dịch khác.


10
Làm thế nào bạn đo lường sự khác biệt? Và bạn đã sử dụng tùy chọn nào khi biên dịch?
James Kanze

158
Tại sao trình biên dịch không giảm +/- 0 trong trường hợp này?!?
Michael Dorgan

127
@ Zyx2000 Trình biên dịch không ở đâu gần đó. Tháo dỡ một ví dụ nhỏ trong LINQPad cho thấy nó spits ra các mã như nhau cho dù bạn sử dụng 0, 0f, 0d, hoặc thậm chí (int)0trong một bối cảnh mà một doublelà cần thiết.
millimoose

14
mức độ tối ưu hóa là gì?
Otto Allmendinger 17/212

Câu trả lời:


1616

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

Các số bất thường (hoặc không bình thường) là một loại hack để có được một số giá trị bổ sung rất gần với số 0 trong biểu diễn dấu phẩy động. Các thao tác trên điểm nổi không chuẩn hóa có thể chậm hơn hàng chục đến hàng trăm lầ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 microcode.

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

Đây là mã kiểm tra đượ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 số rất gần với không.

Các số không chuẩn hóa thường rất 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ứ để làm với các số không chuẩn hóa, nếu chúng ta xóa các biến số thành 0 bằng cách thêm mã này vào đầu mã:

_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

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

Điều này có nghĩa là thay vì sử dụng các giá trị độ chính xác gần như thấp bằng 0 này, chúng ta 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 gì đến việc đó là số nguyên hay dấu phẩy động. Các 0hoặc 0.1fđược chuyển đổi / lưu trữ vào một bên ngoài đăng ký của cả hai vòng. Vì vậy, không có ảnh hưởng đến hiệu suất.


100
Tôi vẫn thấy hơi lạ khi "+ 0" không được tối ưu hóa hoàn toàn 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" không?
s73v3r

51
@ 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ể + 0.0fcó tác dụng phụ nếu y[i]tình cờ là một tín hiệu NaNhoặc một cái gì đó ... tôi có thể sai mặc dù.
Bí ẩn

14
Đôi vẫn sẽ gặp vấn đề tương tự trong nhiều trường hợp, chỉ ở một cường độ số khác nhau. Flush-to-zero là tốt cho các ứng dụng âm thanh (và những ứng dụng khác mà 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, cách khắc phục thông thường cho các ứng dụng âm thanh là đưa tín hiệu DC có biên độ rất thấp (không nghe được) hoặc tín hiệu sóng vuông để các số jitter tránh khỏi biến dạng.
Russell Borogove

16
@Isaac bởi vì khi y [i] nhỏ hơn đáng kể so với 0,1, điều đó dẫn đến mất độ chính xác vì chữ số có nghĩa nhất trong số trở nên cao hơn.
Dan đang loay hoay bởi Firelight 17/212

167
@ s73v3r: Không thể tối ưu hóa + 0.f 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, 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

415

Sử dụng gccvà áp dụng một khác biệt cho lắp ráp được tạo ra chỉ mang lại sự khác biệt này:

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

Người cvtsi2ssqthực sự chậm hơn 10 lần.

Rõ ràng, floatphiên bản sử dụng một thanh ghi XMM được tải từ bộ nhớ, trong khi intphiên bản chuyển đổi intgiá trị thực 0 thành floatsử dụng cvtsi2ssqhướng dẫn, mất rất nhiều thời gian. Chuyển -O3đến gcc không giúp được gì. (phiên bản gcc 4.2.1.)

(Sử dụng doublethay vì floatkhông quan trọng, ngoại trừ việc nó thay đổi cvtsi2ssqthà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 là cvtsi2ssqhướng dẫn. Sau khi loại bỏ (sử dụng a int ai=0;float a=ai;và sử dụng athay 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 ra 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 00.1f. Bước ngoặt trong đoạn mã trên xấp xỉ lúc 0.00000000000000000000000000000001, khi các vòng lặp đột ngột mất 10 lần thời gian.

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 số float, chia cho 2 cho mỗi lần lặp
  • Cột 2: biểu diễn nhị phân của float này
  • Cột 3: thời gian thực hiện để tổng số float 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 quá trình không chuẩn hóa đặt vào. Tại thời điểm đó, phép cộng đơn giản trở nên 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 bất thường trong Objective-C? .


27
-OKhông sửa nó, nhưng -ffast-mathkhông. (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 sự cố chính xác không nên xuất hiện trong một chương trình được thiết kế đúng cách.)
leftaroundabout 17/212

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

@leftaroundabout: biên dịch một tệp thực thi (không phải thư viện) với -ffast-mathcác liên kết một số mã khởi động bổ sung đặt FTZ (tuôn ra 0) và DAZ (không bình thường bằng 0) trong MXCSR, do đó CPU không bao giờ phải hỗ trợ vi mã chậm cho biến dạng.
Peter Cordes

34

Đó 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ả nó và hình phạt hiệu suất? Đã lùng sục trên Internet những cách để giết những con số bất thường, dường như vẫn chưa có cách "tốt nhất" nào để 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 để hoạt động 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 biến dạng theo mặc định trên các CPU Intel hiện đại. Thêm chi tiết tại đây

  • Trình biên dịch chuyển mạch. -ffast-math, -msseHoặc -mfpmath=ssesẽ 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 mà 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:fastnhưng tôi chưa thể xác nhận liệu điều này cũng vô hiệu hóa các biến đổi. 1


1
Điều này nghe có vẻ như là một câu trả lời đúng cho một câu hỏi khác nhưng có liên quan (Làm thế nào tôi có thể ngăn các tính toán số tạo ra kết quả không bình thường?) Tuy nhiên, nó không trả lời câu hỏi này.
Ben Voigt

Windows X64 vượt qua cài đặt đột ngột khi nó khởi chạy .exe, trong khi Windows 32-bit và linux thì không. Trên linux, gcc -ffast-math nên đặt 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 chính () để những khác biệt về hệ điều hành này không vượt qua, nhưng tôi đã bị cắn và cần phải thiết lập rõ ràng trong chương trình. Các CPU Intel bắt đầu với Sandy Bridge được cho là xử lý các subnorm phát sinh trong phép 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 chảy dần dần.
tim18

1
Microsoft / fp: fast (không phải mặc định) không thực hiện 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: fast. Nó giống như ICL / fp: nguồn. Vì vậy, bạn phải đặt / fp: (và, trong một số trường hợp, chế độ dưới dòng) một cách rõ ràng nếu bạn muốn so sánh các trình biên dịch này.
tim18

18

Trong gcc, bạn có thể kích hoạt FTZ và DAZ bằng cách 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ác 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


Đồng thời xem fesetround()từ fenv.h(được xác định cho C99) để biết cách làm tròn khác, di động hơ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ỉ là các chương trình con )
German Garcia

Bạn có chắc chắn cần 1 << 15 và 1 << 11 cho FTZ không? Tôi mới chỉ thấy 1 << 15 trích dẫn ở nơi khác ...
fig

@fig: 1 << 11 dành cho Mặt nạ dưới. Thêm thông tin tại đây: softpixel.com/~cwright/programming/simd/sse.php
German Garcia

@GermanGarcia điều này không trả lời câu hỏi OP; câu hỏi là "Tại sao bit mã này, 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 giải quyết này hoặc cung cấp điều này trong một nhận xét.

9

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ố không 0.0fđược chuẩn hóa hoặc gây ra sự chậm lại, đó là các giá trị tiếp cận 0 mỗi lần lặp của vòng lặp. Khi chúng đến gần hơn và gần hơn với số 0, chúng cần độ chính xác cao 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ến gần đến 0 vì x[i]/z[i]ít 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 hiện mỗi lần lặp của vòng lặp, độ chính xác bổ sung trong float sẽ bị mất và sự không chuẩn hóa cần thiết để thể hiện độ chính xác đó là không còn cần thiết. Sau đó, các hoạt động của dấu phẩy động y[i]vẫn duy trì 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? Bởi vì số dấu phẩy động chỉ có rất nhiều chữ số có nghĩa. Giả sử bạn có đủ dung lượng lưu trữ cho ba chữ số có nghĩa, sau đó 0.00001 = 1e-5, và 0.00001 + 0.1 = 0.1, ít nhất là đối với định dạng float ví dụ này, vì nó không có chỗ để lưu trữ bit đáng kể nhất 0.10001.

Nói tóm lại, y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;không phải là không có bạn nghĩ nó là.

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

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.