Tại sao sqrt vô hướng SSE (x) chậm hơn rsqrt (x) * x?


106

Tôi đã lập hồ sơ một số phép toán cốt lõi của chúng tôi trên Intel Core Duo và trong khi xem xét các cách tiếp cận khác nhau đối với căn bậc hai, tôi đã nhận thấy một điều kỳ lạ: sử dụng các phép toán vô hướng SSE, nhanh hơn để lấy căn bậc hai đối ứng và nhân nó để lấy sqrt, thay vì sử dụng opcode sqrt gốc!

Tôi đang kiểm tra nó với một vòng lặp như:

inline float TestSqrtFunction( float in );

void TestFunc()
{
  #define ARRAYSIZE 4096
  #define NUMITERS 16386
  float flIn[ ARRAYSIZE ]; // filled with random numbers ( 0 .. 2^22 )
  float flOut [ ARRAYSIZE ]; // filled with 0 to force fetch into L1 cache

  cyclecounter.Start();
  for ( int i = 0 ; i < NUMITERS ; ++i )
    for ( int j = 0 ; j < ARRAYSIZE ; ++j )
    {
       flOut[j] = TestSqrtFunction( flIn[j] );
       // unrolling this loop makes no difference -- I tested it.
    }
  cyclecounter.Stop();
  printf( "%d loops over %d floats took %.3f milliseconds",
          NUMITERS, ARRAYSIZE, cyclecounter.Milliseconds() );
}

Tôi đã thử điều này với một số cơ quan khác nhau cho TestSqrtFunction và tôi có một số thời gian thực sự khiến tôi đau đầu. Điều tồi tệ nhất cho đến nay là sử dụng hàm sqrt () gốc và để trình biên dịch "thông minh" "tối ưu hóa". Ở 24ns / float, sử dụng FPU x87, điều này thật tệ hại:

inline float TestSqrtFunction( float in )
{  return sqrt(in); }

Điều tiếp theo tôi đã thử là sử dụng nội tại để buộc trình biên dịch sử dụng opcode sqrt vô hướng của SSE:

inline void SSESqrt( float * restrict pOut, float * restrict pIn )
{
   _mm_store_ss( pOut, _mm_sqrt_ss( _mm_load_ss( pIn ) ) );
   // compiles to movss, sqrtss, movss
}

Điều này tốt hơn, ở mức 11,9ns / float. Tôi cũng đã thử kỹ thuật xấp xỉ Newton-Raphson lập dị của Carmack , chạy thậm chí còn tốt hơn cả phần cứng, ở mức 4,3ns / float, mặc dù với lỗi 1 trong 2 10 (quá nhiều so với mục đích của tôi).

Doozy là khi tôi thử chọn SSE cho căn bậc hai nghịch đảo , và sau đó sử dụng một phép nhân để lấy căn bậc hai (x * 1 / √x = √x). Mặc dù điều này cần đến hai phép toán phụ thuộc, nhưng nó là giải pháp nhanh nhất cho đến nay, với tốc độ 1,24ns / float và chính xác đến 2 -14 :

inline void SSESqrt_Recip_Times_X( float * restrict pOut, float * restrict pIn )
{
   __m128 in = _mm_load_ss( pIn );
   _mm_store_ss( pOut, _mm_mul_ss( in, _mm_rsqrt_ss( in ) ) );
   // compiles to movss, movaps, rsqrtss, mulss, movss
}

Câu hỏi của tôi về cơ bản là những gì cho ? Tại sao opcode căn bậc hai tích hợp trong phần cứng của SSE lại chậm hơn so với việc tổng hợp nó từ hai phép toán khác?

Tôi chắc chắn rằng đây thực sự là chi phí của chính op, bởi vì tôi đã xác minh:

  • Tất cả dữ liệu đều nằm trong bộ nhớ cache và các truy cập là tuần tự
  • các chức năng được nội tuyến
  • mở vòng lặp không tạo ra sự khác biệt
  • cờ trình biên dịch được đặt thành tối ưu hóa hoàn toàn (và lắp ráp tốt, tôi đã kiểm tra)

( chỉnh sửa : stephentyrone chỉ ra một cách chính xác rằng các hoạt động trên chuỗi số dài nên sử dụng các hoạt động đóng gói SIMD vectơ hóa, chẳng hạn như rsqrtps- nhưng cấu trúc dữ liệu mảng ở đây chỉ dành cho mục đích thử nghiệm: điều tôi thực sự đang cố gắng đo lường là hiệu suất vô hướng để sử dụng trong mã không thể được vector hóa.)


13
x / sqrt (x) = sqrt (x). Hoặc, nói một cách khác: x ^ 1 * x ^ (- 1/2) = x ^ (1 - 1/2) = x ^ (1/2) = sqrt (x)
Crashworks 29/10/09

6
tất nhiên inline float SSESqrt( float restrict fIn ) { float fOut; _mm_store_ss( &fOut, _mm_sqrt_ss( _mm_load_ss( &fIn ) ) ); return fOut; },. Nhưng đây là một ý tưởng tồi vì nó có thể dễ dàng gây ra tình trạng ngưng trệ tải trọng nếu CPU ghi các float vào ngăn xếp và sau đó đọc lại chúng ngay lập tức - tung hứng từ thanh ghi vectơ sang thanh ghi float cho giá trị trả về cụ thể là một tin xấu. Bên cạnh đó, các mã quang máy cơ bản mà bản chất SSE đại diện vẫn lấy toán hạng địa chỉ.
Crashworks

4
Mức độ quan trọng của LHS phụ thuộc vào thế hệ cụ thể và bước của một x86 nhất định: kinh nghiệm của tôi là trên bất kỳ thứ gì lên đến i7, việc di chuyển dữ liệu giữa các bộ thanh ghi (ví dụ: FPU sang SSE đến eax) là rất tệ, trong khi một vòng giữa xmm0 và ngăn xếp và quay lại thì không, vì chuyển tiếp cửa hàng của Intel. Bạn có thể tự thời gian để xem cho chắc chắn. Nói chung, cách dễ nhất để xem LHS tiềm năng là nhìn vào cụm được phát ra và xem nơi dữ liệu được ghép giữa các bộ thanh ghi; trình biên dịch của bạn có thể làm điều thông minh hoặc có thể không. Để chuẩn hóa vectơ, tôi đã viết kết quả của mình ở đây: bit.ly/9W5zoU
Crashworks

2
Đối với PowerPC, có: IBM có một trình mô phỏng CPU có thể dự đoán LHS và nhiều bong bóng đường ống khác thông qua phân tích tĩnh. Một số PPC cũng có bộ đếm phần cứng cho LHS mà bạn có thể thăm dò ý kiến. Nó khó hơn cho x86; các công cụ cấu hình tốt ngày càng khan hiếm hơn (VTune ngày nay hơi bị hỏng) và các đường ống được sắp xếp lại ít xác định hơn. Bạn có thể thử đo nó theo kinh nghiệm bằng cách đo hướng dẫn mỗi chu kỳ, điều này có thể được thực hiện chính xác với bộ đếm hiệu suất phần cứng. Có thể đọc các thanh ghi "hướng dẫn đã ngừng hoạt động" và "tổng chu kỳ" bằng ví dụ PAPI hoặc PerfSuite ( bit.ly/an6cMt ).
Crashworks

2
Bạn cũng có thể chỉ cần viết một vài hoán vị trên một hàm và định thời gian cho chúng để xem liệu có cái nào bị lỗi đặc biệt không. Intel không công bố nhiều thông tin chi tiết về cách thức hoạt động của các đường ống dẫn của họ (rằng họ LHS hoàn toàn là một bí mật bẩn thỉu), vì vậy rất nhiều điều tôi học được là bằng cách xem xét một kịch bản gây ra sự cố trên các cổng khác (ví dụ: PPC ), và sau đó xây dựng một thử nghiệm được kiểm soát để xem liệu x86 cũng có.
Crashworks

Câu trả lời:


216

sqrtsscho một kết quả làm tròn chính xác. rsqrtssđưa ra giá trị gần đúng với nghịch đảo, chính xác đến khoảng 11 bit.

sqrtssđang tạo ra kết quả chính xác hơn rất nhiều, khi cần độ chính xác. rsqrtsstồn tại cho các trường hợp khi một số gần đúng là đủ, nhưng tốc độ là bắt buộc. Nếu bạn đọc tài liệu của Intel, bạn cũng sẽ tìm thấy một chuỗi lệnh (xấp xỉ căn bậc hai nghịch đảo theo sau bởi một bước Newton-Raphson duy nhất) cung cấp độ chính xác gần như đầy đủ (độ chính xác ~ 23 bit, nếu tôi nhớ đúng) và vẫn còn nhanh hơn sqrtss.

chỉnh sửa: Nếu tốc độ là quan trọng và bạn thực sự gọi điều này trong một vòng lặp cho nhiều giá trị, bạn nên sử dụng các phiên bản vector hóa của các hướng dẫn này rsqrtpshoặc sqrtpscả hai đều xử lý bốn float cho mỗi lệnh.


3
Bước n / r cung cấp cho bạn độ chính xác 22 bit (nó tăng gấp đôi); 23-bit sẽ là độ chính xác hoàn toàn.
Jasper Bekkers

7
@Jasper Bekkers: Không, sẽ không. Đầu tiên, float có 24 bit chính xác. Thứ hai, sqrtssđược làm tròn chính xác , yêu cầu ~ 50 bit trước khi làm tròn và không thể đạt được bằng cách sử dụng phép lặp N / R đơn giản với độ chính xác duy nhất.
Stephen Canon

1
Đây chắc chắn là lý do. Để mở rộng kết quả này: Dự án Embree của Intel ( software.intel.com/en-us/articles/… ), sử dụng vectơ hóa cho toán học của mình. Bạn có thể tải xuống nguồn tại liên kết đó và xem cách họ làm Vectơ 3/4 D của họ. Chuẩn hóa vectơ của họ sử dụng rsqrt, theo sau là một lần lặp lại newton-raphson, sau đó rất chính xác và vẫn nhanh hơn 1 / ssqrt!
Brandon Pelfrey

7
Một lưu ý nhỏ: x rsqrt (x) cho kết quả là NaN nếu x bằng 0 hoặc vô cùng. 0 * rsqrt (0) = 0 * INF = NaN. INF rsqrt (INF) = INF * 0 = NaN. Vì lý do này, CUDA trên GPU NVIDIA tính toán các căn bậc hai chính xác đơn lẻ gần đúng dưới dạng nghịch đảo (rsqrt (x)), với phần cứng cung cấp cả giá trị xấp xỉ nhanh cho căn bậc hai nghịch đảo và nghịch đảo. Rõ ràng, các kiểm tra rõ ràng xử lý hai trường hợp đặc biệt cũng có thể (nhưng sẽ chậm hơn trên GPU).
njuffa

@BrandonPelfrey Bạn đã tìm thấy bước Newton Rhapson trong tệp nào?
fredoverflow 14/1213

7

Điều này cũng đúng với sự phân chia. MULSS (a, RCPSS (b)) nhanh hơn DIVSS (a, b). Trên thực tế, nó vẫn nhanh hơn ngay cả khi bạn tăng độ chính xác của nó bằng phép lặp Newton-Raphson.

Cả Intel và AMD đều khuyến nghị kỹ thuật này trong sách hướng dẫn tối ưu hóa của họ. Trong các ứng dụng không yêu cầu tuân thủ IEEE-754, lý do duy nhất để sử dụng div / sqrt là khả năng đọc mã.


1
Broadwell và sau đó có hiệu suất phân chia FP tốt hơn, vì vậy các trình biên dịch như clang chọn không sử dụng đối ứng + Newton cho vô hướng trên các CPU gần đây, vì nó thường không nhanh hơn. Trong hầu hết các vòng lặp, divkhông phải là hoạt động duy nhất, vì vậy tổng thông lượng tối ưu thường là nút cổ chai ngay cả khi có divpshoặc divss. Xem phần Chia dấu phẩy động và phép nhân dấu phẩy động , trong đó câu trả lời của tôi có một phần về lý do tại sao rcppskhông phải là chiến thắng thông lượng nữa. (Hoặc chiến thắng về độ trễ) và các con số về phân chia thông lượng / độ trễ.
Peter Cordes

Nếu yêu cầu về độ chính xác của bạn thấp đến mức bạn có thể bỏ qua một lần lặp Newton, thì có a * rcpss(b)thể nhanh hơn, nhưng nó vẫn còn nhiều hơn a/b!
Peter Cordes

5

Thay vì cung cấp một câu trả lời, điều đó thực sự có thể không chính xác (tôi cũng sẽ không kiểm tra hoặc tranh luận về bộ nhớ cache và những thứ khác, giả sử chúng giống hệt nhau) Tôi sẽ cố gắng chỉ cho bạn nguồn có thể trả lời câu hỏi của bạn.
Sự khác biệt có thể nằm ở cách tính toán sqrt và rsqrt. Bạn có thể đọc thêm tại đây http://www.intel.com/products/processor/manuals/ . Tôi khuyên bạn nên bắt đầu từ việc đọc các chức năng của bộ xử lý mà bạn đang sử dụng, có một số thông tin, đặc biệt là về rsqrt (cpu đang sử dụng bảng tra cứu nội bộ với giá trị xấp xỉ rất lớn, giúp việc lấy kết quả đơn giản hơn nhiều). Có vẻ như rsqrt nhanh hơn sqrt rất nhiều, nên 1 phép toán mul bổ sung (không tốn kém) có thể không thay đổi tình hình ở đây.

Chỉnh sửa: Một vài sự kiện có thể đáng nói:
1. Một khi tôi đang thực hiện một số tối ưu hóa vi mô cho thư viện đồ họa của mình và tôi đã sử dụng rsqrt để tính toán độ dài của vectơ. (thay vì sqrt, tôi đã nhân tổng bình phương của mình với rsqrt của nó, chính xác là những gì bạn đã làm trong các thử nghiệm của mình) và nó hoạt động tốt hơn.
2. Tính toán rsqrt bằng cách sử dụng bảng tra cứu đơn giản có thể dễ dàng hơn, như đối với rsqrt, khi x chuyển sang vô cùng, 1 / sqrt (x) chuyển sang 0, vì vậy đối với x nhỏ, các giá trị hàm không thay đổi (nhiều), trong khi đối với sqrt - nó đi đến vô cùng, vì vậy nó là trường hợp đơn giản;).

Ngoài ra, giải thích rõ: Tôi không chắc mình đã tìm thấy nó ở đâu trong những cuốn sách mà tôi đã liên kết, nhưng tôi khá chắc chắn rằng tôi đã đọc rằng rsqrt đang sử dụng một số bảng tra cứu và nó chỉ nên được sử dụng khi có kết quả không cần phải chính xác, mặc dù - tôi cũng có thể sai, vì nó đã xảy ra một thời gian trước đây :).


4

Newton-Raphson hội tụ về 0 f(x)bằng cách sử dụng gia số bằng với -f/f' nơif' là đạo hàm.

Đối với x=sqrt(y), bạn có thể cố gắng giải quyết f(x) = 0để xsử dụng f(x) = x^2 - y;

Sau đó, gia số là: dx = -f/f' = 1/2 (x - y/x) = 1/2 (x^2 - y) / x có sự phân chia chậm trong đó.

Bạn có thể thử các chức năng khác (như f(x) = 1/y - 1/x^2) nhưng chúng sẽ phức tạp không kém.

Hãy nhìn vào 1/sqrt(y)bây giờ. Bạn có thể thử f(x) = x^2 - 1/y, nhưng nó sẽ phức tạp không kém: dx = 2xy / (y*x^2 - 1)chẳng hạn. Một lựa chọn thay thế không rõ ràng f(x)là:f(x) = y - 1/x^2

Sau đó: dx = -f/f' = (y - 1/x^2) / (2/x^3) = 1/2 * x * (1 - y * x^2)

Ah! Đó không phải là một biểu thức tầm thường, nhưng bạn chỉ có phép nhân trong đó, không có phép chia. => Nhanh hơn!

Và: bước cập nhật đầy đủ new_x = x + dxsau đó đọc:

x *= 3/2 - y/2 * x * x mà cũng dễ dàng.


2

Có một số câu trả lời khác cho vấn đề này đã có từ vài năm trước. Đây là những gì mà sự đồng thuận đã đúng:

  • Các lệnh rsqrt * tính toán một giá trị gần đúng với căn bậc hai nghịch đảo, tốt cho khoảng 11-12 bit.
  • Nó được thực hiện với một bảng tra cứu (tức là một ROM) được lập chỉ mục bởi phần định trị. (Trên thực tế, đó là một bảng tra cứu nén, tương tự như các bảng toán học cũ, sử dụng các điều chỉnh đối với các bit bậc thấp để tiết kiệm trên các bóng bán dẫn.)
  • Lý do tại sao nó có sẵn là vì nó là ước tính ban đầu được FPU sử dụng cho thuật toán căn bậc hai "thực".
  • Ngoài ra còn có một lệnh tương hỗ gần đúng, rcp. Cả hai hướng dẫn này đều là manh mối về cách FPU triển khai căn bậc hai và phép chia.

Đây là những gì mà sự đồng thuận đã sai:

  • Các FPU thời SSE không sử dụng Newton-Raphson để tính căn bậc hai. Đó là một phương pháp tuyệt vời trong phần mềm, nhưng sẽ là một sai lầm nếu thực hiện nó theo cách đó trong phần cứng.

Thuật toán NR để tính căn bậc hai đối ứng có bước cập nhật này, như những người khác đã lưu ý:

x' = 0.5 * x * (3 - n*x*x);

Đó là rất nhiều phép nhân phụ thuộc dữ liệu và một phép trừ.

Sau đây là thuật toán mà FPU hiện đại thực sự sử dụng.

Cho trước b[0] = n, giả sử chúng ta có thể tìm thấy một dãy số Y[i]sao cho b[n] = b[0] * Y[0]^2 * Y[1]^2 * ... * Y[n]^2gần bằng 1. Sau đó hãy xem xét:

x[n] = b[0] * Y[0] * Y[1] * ... * Y[n]
y[n] = Y[0] * Y[1] * ... * Y[n]

Rõ ràng x[n]cách tiếp cận sqrt(n)y[n]phương pháp tiếp cận 1/sqrt(n).

Chúng ta có thể sử dụng bước cập nhật Newton-Raphson cho căn bậc hai nghịch đảo để có được Y[i]:

b[i] = b[i-1] * Y[i-1]^2
Y[i] = 0.5 * (3 - b[i])

Sau đó:

x[0] = n Y[0]
x[i] = x[i-1] * Y[i]

và:

y[0] = Y[0]
y[i] = y[i-1] * Y[i]

Quan sát quan trọng tiếp theo là đó b[i] = x[i-1] * y[i-1]. Vì thế:

Y[i] = 0.5 * (3 - x[i-1] * y[i-1])
     = 1 + 0.5 * (1 - x[i-1] * y[i-1])

Sau đó:

x[i] = x[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = x[i-1] + x[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))
y[i] = y[i-1] * (1 + 0.5 * (1 - x[i-1] * y[i-1]))
     = y[i-1] + y[i-1] * 0.5 * (1 - x[i-1] * y[i-1]))

Tức là, với x và y ban đầu, chúng ta có thể sử dụng bước cập nhật sau:

r = 0.5 * (1 - x * y)
x' = x + x * r
y' = y + y * r

Hoặc, thậm chí lạ hơn, chúng tôi có thể đặt h = 0.5 * y. Đây là khởi tạo:

Y = approx_rsqrt(n)
x = Y * n
h = Y * 0.5

Và đây là bước cập nhật:

r = 0.5 - x * h
x' = x + x * r
h' = h + h * r

Đây là thuật toán của Goldschmidt và nó có một lợi thế rất lớn nếu bạn đang triển khai nó trong phần cứng: "vòng lặp bên trong" là ba phép nhân-cộng và không có gì khác, và hai trong số chúng là độc lập và có thể được kết nối.

Vào năm 1999, các FPU đã cần một mạch cộng / rút ngắn có trục và mạch nhân có trục, nếu không SSE sẽ không "phát trực tuyến" cho lắm. Chỉ cần một trong mỗi mạch vào năm 1999 để thực hiện vòng lặp bên trong này theo cách hoàn chỉnh mà không lãng phí nhiều phần cứng chỉ tính trên căn bậc hai.

Tất nhiên, hôm nay chúng ta đã kết hợp nhân-cộng tiếp xúc với lập trình viên. Một lần nữa, vòng lặp bên trong là ba FMA có trục, thường (một lần nữa) thường hữu ích ngay cả khi bạn không tính căn bậc hai.


1
Liên quan: Sqrt () của GCC hoạt động như thế nào sau khi biên dịch? Sử dụng phương pháp root nào? Newton-Raphson? có một số liên kết đến thiết kế đơn vị thực thi div / sqrt phần cứng. Rsqrt được vectơ hóa nhanh và đối ứng với SSE / AVX tùy thuộc vào độ chính xác - một lần lặp Newton trong phần mềm, có hoặc không có FMA, để sử dụng với _mm256_rsqrt_psphân tích hiệu suất Haswell. Thường chỉ là một ý tưởng hay nếu bạn không có công việc khác trong vòng lặp và sẽ làm tắc nghẽn khó khăn về thông lượng của bộ chia. HW sqrt là một uop duy nhất nên được kết hợp với các công việc khác.
Peter Cordes

-2

Sẽ nhanh hơn vì hướng dẫn này bỏ qua các chế độ làm tròn và không xử lý các ngoại lệ của điểm floatin hoặc các số chuẩn hóa. Vì những lý do này, việc chuyển hướng, suy đoán và thực hiện lệnh fp khác không theo thứ tự dễ dàng hơn nhiều.


Rõ ràng là sai. FMA phụ thuộc vào chế độ làm tròn hiện tại, nhưng có thông lượng là hai trên mỗi đồng hồ trên Haswell trở lên. Với hai đơn vị FMA được kết nối đầy đủ, Haswell có thể có tới 10 FMA trong chuyến bay cùng một lúc. Câu trả lời đúng là rsqrt's nhiều độ chính xác thấp hơn, có nghĩa là ít hơn nhiều việc phải làm (hoặc không có gì cả?) Sau khi một bảng tra cứu để có được một dự đoán ban đầu.
Peter Cordes
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.