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.)
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ỉ.
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