Tính toán dấu phẩy động so với số nguyên trên phần cứng hiện đại


100

Tôi đang thực hiện một số công việc quan trọng về hiệu suất trong C ++ và chúng tôi hiện đang sử dụng các phép tính số nguyên cho các vấn đề vốn có dấu phẩy động vì "nó nhanh hơn". Điều này gây ra rất nhiều vấn đề khó chịu và thêm rất nhiều mã khó chịu.

Bây giờ, tôi nhớ đã đọc về cách các phép tính dấu phẩy động chậm đến mức vào khoảng 386 ngày, nơi mà tôi tin (IIRC) rằng có một bộ đồng xử lý tùy chọn. Nhưng chắc chắn ngày nay với các CPU phức tạp và mạnh mẽ hơn theo cấp số nhân thì không có gì khác biệt về "tốc độ" nếu thực hiện phép tính dấu phẩy động hoặc số nguyên? Đặc biệt là vì thời gian tính toán thực tế là rất nhỏ so với một cái gì đó như gây ra sự cố đường ống hoặc tìm nạp thứ gì đó từ bộ nhớ chính?

Tôi biết câu trả lời chính xác là điểm chuẩn trên phần cứng mục tiêu, cách tốt để kiểm tra điều này là gì? Tôi đã viết hai chương trình C ++ nhỏ và so sánh thời gian chạy của chúng với "thời gian" trên Linux, nhưng thời gian chạy thực tế quá thay đổi (không giúp tôi chạy trên máy chủ ảo). Tôi không phải dành cả ngày để chạy hàng trăm điểm chuẩn, vẽ biểu đồ, v.v., vậy tôi có thể làm gì để có được một bài kiểm tra hợp lý về tốc độ tương đối không? Bất kỳ ý tưởng hoặc suy nghĩ? Tôi có sai hoàn toàn không?

Các chương trình tôi đã sử dụng như sau, chúng không giống nhau bởi bất kỳ phương tiện nào:

#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>

int main( int argc, char** argv )
{
    int accum = 0;

    srand( time( NULL ) );

    for( unsigned int i = 0; i < 100000000; ++i )
    {
        accum += rand( ) % 365;
    }
    std::cout << accum << std::endl;

    return 0;
}

Chương trình 2:

#include <iostream>
#include <cmath>
#include <cstdlib>
#include <time.h>

int main( int argc, char** argv )
{

    float accum = 0;
    srand( time( NULL ) );

    for( unsigned int i = 0; i < 100000000; ++i )
    {
        accum += (float)( rand( ) % 365 );
    }
    std::cout << accum << std::endl;

    return 0;
}

Cảm ơn trước!

Chỉnh sửa: Nền tảng tôi quan tâm là x86 hoặc x86-64 thông thường chạy trên máy tính để bàn Linux và Windows.

Chỉnh sửa 2 (dán từ một bình luận bên dưới): Chúng tôi hiện có một cơ sở mã mở rộng. Thực sự tôi đã đưa ra chống lại sự tổng quát hóa rằng chúng ta "không được sử dụng float vì phép tính số nguyên nhanh hơn" - và tôi đang tìm cách (nếu điều này thậm chí đúng) để bác bỏ giả thiết tổng quát này. Tôi nhận ra rằng sẽ không thể dự đoán chính xác kết quả đối với chúng tôi nếu không thực hiện tất cả công việc và lập hồ sơ sau đó.

Dù sao, cảm ơn vì tất cả những câu trả lời xuất sắc và sự giúp đỡ của bạn. Hãy thoải mái thêm bất cứ điều gì khác :).


8
Những gì bạn có như bài kiểm tra của bạn bây giờ là tầm thường. Cũng có thể có rất ít sự khác biệt trong lắp ráp, ( ví dụ: được addlthay thế bằng fadd). Cách duy nhất để thực sự có được một phép đo tốt là lấy một phần cốt lõi của chương trình thực của bạn và lập hồ sơ các phiên bản khác nhau của nó. Thật không may, điều đó có thể khá khó khăn nếu không sử dụng rất nhiều nỗ lực. Có lẽ việc cho chúng tôi biết phần cứng mục tiêu và trình biên dịch của bạn sẽ giúp mọi người ít nhất cung cấp cho bạn trải nghiệm đã có từ trước, v.v. Về việc sử dụng số nguyên của bạn, tôi nghi ngờ bạn có thể tạo một loại fixed_pointlớp mẫu sẽ dễ dàng hơn rất nhiều.
GManNickG

1
Vẫn còn rất nhiều kiến ​​trúc không có phần cứng dấu phẩy động chuyên dụng - một số thẻ giải thích hệ thống bạn quan tâm sẽ giúp bạn có câu trả lời tốt hơn.
Carl Norum,

3
Tôi tin rằng phần cứng trong HTC Hero (android) của tôi không có FPU, nhưng phần cứng trong Google NexusOne (android) thì có. mục tiêu của bạn là gì? máy tính để bàn / máy chủ? netbook (có thể arm + linux)? những cái điện thoại?
SteelBytes

5
Nếu bạn muốn FP nhanh trên x86, hãy cố gắng biên dịch với tối ưu hóa và tạo mã SSE. SSE (bất kỳ phiên bản nào) có thể thực hiện phép cộng, trừ và nhân ít nhất là float trong một chu kỳ. Chia, mod và các chức năng cao hơn sẽ luôn chậm. Cũng lưu ý rằng nó floatđược tăng tốc độ, nhưng thường doublethì không.
Mike D.

1
Số nguyên điểm cố định xấp xỉ FP bằng cách sử dụng nhiều phép toán số nguyên để giữ kết quả không bị tràn. Điều đó hầu như luôn chậm hơn so với việc chỉ sử dụng FPU cực kỳ có khả năng được tìm thấy trong các CPU máy tính để bàn hiện đại. Ví dụ: MAD, bộ giải mã mp3 điểm cố định, chậm hơn libmpg123 và mặc dù chất lượng tốt cho bộ giải mã điểm cố định, libmpg123 vẫn ít bị lỗi làm tròn hơn. wezm.net/technical/2008/04/mp3-decoder-libraries-compared cho điểm chuẩn trên PPC G5.
Peter Cordes

Câu trả lời:


35

Than ôi, tôi chỉ có thể cho bạn một câu trả lời "nó phụ thuộc" ...

Theo kinh nghiệm của tôi, có rất nhiều, rất nhiều biến cho hiệu suất ... đặc biệt là giữa toán học số nguyên và dấu phẩy động. Nó thay đổi mạnh mẽ giữa các bộ xử lý (ngay cả trong cùng một họ chẳng hạn như x86) vì các bộ xử lý khác nhau có độ dài "đường ống" khác nhau. Ngoài ra, một số hoạt động thường rất đơn giản (chẳng hạn như phép cộng) và có một lộ trình được tăng tốc qua bộ xử lý, và những hoạt động khác (chẳng hạn như phép chia) mất nhiều thời gian hơn.

Biến lớn khác là nơi dữ liệu cư trú. Nếu bạn chỉ có một vài giá trị cần thêm, thì tất cả dữ liệu có thể nằm trong bộ nhớ cache, nơi chúng có thể nhanh chóng được gửi đến CPU. Một hoạt động dấu phẩy động rất, rất chậm đã có dữ liệu trong bộ nhớ cache sẽ nhanh hơn nhiều lần so với một hoạt động số nguyên trong đó một số nguyên cần được sao chép từ bộ nhớ hệ thống.

Tôi giả định rằng bạn đang hỏi câu hỏi này bởi vì bạn đang làm việc trên một ứng dụng quan trọng về hiệu suất. Nếu bạn đang phát triển cho kiến ​​trúc x86 và bạn cần thêm hiệu suất, bạn có thể muốn xem xét việc sử dụng các phần mở rộng SSE. Điều này có thể tăng tốc đáng kể số học dấu phẩy động chính xác một lần, vì cùng một phép toán có thể được thực hiện trên nhiều dữ liệu cùng một lúc, cộng với một ngân hàng thanh ghi * riêng biệt cho các phép toán SSE. (Tôi nhận thấy trong ví dụ thứ hai của bạn, bạn đã sử dụng "float" thay vì "double", khiến tôi nghĩ rằng bạn đang sử dụng phép toán chính xác đơn).

* Lưu ý: Việc sử dụng các hướng dẫn MMX cũ sẽ thực sự làm chậm các chương trình, vì các lệnh cũ đó thực sự sử dụng cùng các thanh ghi như FPU, khiến bạn không thể sử dụng cả FPU và MMX cùng một lúc.


8
Và trên một số bộ xử lý, phép toán FP có thể nhanh hơn phép toán số nguyên. Bộ xử lý Alpha có lệnh chia FP nhưng không phải là số nguyên, vì vậy phép chia số nguyên phải được thực hiện trong phần mềm.
Gabe

SSEx cũng sẽ tăng tốc độ chính xác gấp đôi số học? Tôi xin lỗi, tôi không quá quen thuộc với SSE
Johannes Schaub - litb

1
@ JohannesSchaub-litb: SSE2 (đường cơ sở cho x86-64) đã đóng gói doubleFP-chính xác. Chỉ với hai 64 bit doubles trên mỗi thanh ghi, tốc độ tăng tiềm năng nhỏ hơn so floatvới mã vectơ hóa tốt. Vô hướng floatdoublesử dụng thanh ghi XMM trên x86-64, với x87 kế thừa chỉ được sử dụng cho long double. (Vì vậy @ Dan: không, thanh ghi MMX không xung đột với thanh ghi FPU bình thường, vì FPU bình thường trên x86-64 là đơn vị SSE. MMX sẽ vô nghĩa vì nếu bạn có thể thực hiện SIMD số nguyên, bạn muốn 16 byte xmm0..15thay vì 8 -byte mm0..7và các CPU hiện đại có MMX kém hơn thông lượng SSE.)
Peter Cordes

1
Nhưng các lệnh số nguyên MMX và SSE * / AVX2 cạnh tranh cho các đơn vị thực thi giống nhau, vì vậy việc sử dụng cả hai cùng một lúc hầu như không bao giờ hữu ích. Chỉ cần sử dụng các phiên bản XMM / YMM rộng hơn để hoàn thành nhiều công việc hơn. Sử dụng số nguyên SIMD và FP cùng lúc cạnh tranh cho các thanh ghi giống nhau, nhưng x86-64 có 16 trong số chúng. Nhưng giới hạn tổng thông lượng có nghĩa là bạn không thể hoàn thành gấp đôi công việc bằng cách sử dụng song song các đơn vị thực thi số nguyên và FP.
Peter Cordes

49

Ví dụ: (số lượng ít hơn nhanh hơn),

64-bit Intel Xeon X5550 @ 2,67GHz, gcc 4.1.2 -O3

short add/sub: 1.005460 [0]
short mul/div: 3.926543 [0]
long add/sub: 0.000000 [0]
long mul/div: 7.378581 [0]
long long add/sub: 0.000000 [0]
long long mul/div: 7.378593 [0]
float add/sub: 0.993583 [0]
float mul/div: 1.821565 [0]
double add/sub: 0.993884 [0]
double mul/div: 1.988664 [0]

Bộ xử lý 32-bit Dual Core AMD Opteron (tm) 265 @ 1.81GHz, gcc 3.4.6 -O3

short add/sub: 0.553863 [0]
short mul/div: 12.509163 [0]
long add/sub: 0.556912 [0]
long mul/div: 12.748019 [0]
long long add/sub: 5.298999 [0]
long long mul/div: 20.461186 [0]
float add/sub: 2.688253 [0]
float mul/div: 4.683886 [0]
double add/sub: 2.700834 [0]
double mul/div: 4.646755 [0]

Như Dan đã chỉ ra , ngay cả khi bạn chuẩn hóa tần số xung nhịp (tự nó có thể gây hiểu nhầm trong các thiết kế pipelined), kết quả sẽ rất khác nhau dựa trên kiến ​​trúc CPU ( hiệu suất ALU / FPU riêng lẻ , cũng như số ALU / FPU thực tế có sẵn cho mỗi cốt lõi trong thiết kế siêu phương diện ảnh hưởng đến số lượng hoạt động độc lập có thể thực thi song song - yếu tố thứ hai không được thực hiện bởi mã bên dưới vì tất cả các hoạt động bên dưới phụ thuộc tuần tự.)

Tiêu chuẩn hoạt động FPU / ALU của người nghèo:

#include <stdio.h>
#ifdef _WIN32
#include <sys/timeb.h>
#else
#include <sys/time.h>
#endif
#include <time.h>
#include <cstdlib>

double
mygettime(void) {
# ifdef _WIN32
  struct _timeb tb;
  _ftime(&tb);
  return (double)tb.time + (0.001 * (double)tb.millitm);
# else
  struct timeval tv;
  if(gettimeofday(&tv, 0) < 0) {
    perror("oops");
  }
  return (double)tv.tv_sec + (0.000001 * (double)tv.tv_usec);
# endif
}

template< typename Type >
void my_test(const char* name) {
  Type v  = 0;
  // Do not use constants or repeating values
  //  to avoid loop unroll optimizations.
  // All values >0 to avoid division by 0
  // Perform ten ops/iteration to reduce
  //  impact of ++i below on measurements
  Type v0 = (Type)(rand() % 256)/16 + 1;
  Type v1 = (Type)(rand() % 256)/16 + 1;
  Type v2 = (Type)(rand() % 256)/16 + 1;
  Type v3 = (Type)(rand() % 256)/16 + 1;
  Type v4 = (Type)(rand() % 256)/16 + 1;
  Type v5 = (Type)(rand() % 256)/16 + 1;
  Type v6 = (Type)(rand() % 256)/16 + 1;
  Type v7 = (Type)(rand() % 256)/16 + 1;
  Type v8 = (Type)(rand() % 256)/16 + 1;
  Type v9 = (Type)(rand() % 256)/16 + 1;

  double t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v += v0;
    v -= v1;
    v += v2;
    v -= v3;
    v += v4;
    v -= v5;
    v += v6;
    v -= v7;
    v += v8;
    v -= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s add/sub: %f [%d]\n", name, mygettime() - t1, (int)v&1);
  t1 = mygettime();
  for (size_t i = 0; i < 100000000; ++i) {
    v /= v0;
    v *= v1;
    v /= v2;
    v *= v3;
    v /= v4;
    v *= v5;
    v /= v6;
    v *= v7;
    v /= v8;
    v *= v9;
  }
  // Pretend we make use of v so compiler doesn't optimize out
  //  the loop completely
  printf("%s mul/div: %f [%d]\n", name, mygettime() - t1, (int)v&1);
}

int main() {
  my_test< short >("short");
  my_test< long >("long");
  my_test< long long >("long long");
  my_test< float >("float");
  my_test< double >("double");

  return 0;
}

8
tại sao bạn trộn mult và div? Sẽ không thú vị nếu mult có thể (hoặc dự kiến?) Nhanh hơn nhiều so với div?
Kyss Tao

13
Phép nhân nhanh hơn nhiều so với phép chia trong cả trường hợp số nguyên và dấu phẩy động. Hiệu suất phân chia cũng phụ thuộc vào kích thước của các con số. Tôi thường cho rằng phép chia chậm hơn ~ 15 lần.
Sogartar

4
pastebin.com/Kx8WGUfg Tôi đã lấy điểm chuẩn của bạn và tách từng thao tác thành vòng lặp riêng và thêm vào volatileđể đảm bảo. Trên Win64, FPU không được sử dụng và MSVC sẽ không tạo mã cho nó, vì vậy nó biên dịch các lệnh bằng cách sử dụng mulssdivssXMM ở đó, nhanh hơn 25 lần so với FPU trong Win32. Máy kiểm tra là Core i5 M 520 @ 2.40GHz
James Dunne

4
@JamesDunne chỉ cần cẩn thận, đối với các hoạt động fp vsẽ nhanh chóng đạt đến 0 hoặc +/- inf rất nhanh, có thể hoặc có thể không (về mặt lý thuyết) được coi là một trường hợp đặc biệt / nhanh chóng được thực hiện bởi một số triển khai fpu nhất định.
vladr

3
"Điểm chuẩn" này không có dữ liệu song song để thực thi không theo thứ tự, vì mọi hoạt động được thực hiện với cùng một bộ tích lũy ( v). Trên các thiết kế gần đây của Intel, phân chia hoàn toàn không bị phân chia ( divss/ divpscó độ trễ chu kỳ 10-14 và cùng một thông lượng tương hỗ). mulsstuy nhiên là độ trễ 5 chu kỳ, nhưng có thể phát hành một chu kỳ mỗi chu kỳ. (Hoặc hai mỗi chu kỳ trên Haswell, vì cổng 0 và cổng 1 đều có hệ số nhân cho FMA).
Peter Cordes

23

Có thể có sự khác biệt đáng kể về tốc độ trong thế giới thực giữa phép toán dấu chấm động và dấu phẩy động, nhưng thông lượng trường hợp lý thuyết tốt nhất của ALU và FPU hoàn toàn không liên quan. Thay vào đó, số lượng thanh ghi số nguyên và dấu phẩy động (thanh ghi thực, không phải tên thanh ghi) trên kiến ​​trúc của bạn không được sử dụng bởi tính toán của bạn (ví dụ: đối với điều khiển vòng lặp), số phần tử của mỗi loại nằm trong một dòng bộ nhớ cache , tối ưu hóa có thể xem xét các ngữ nghĩa khác nhau cho phép toán số nguyên và dấu phẩy động - những hiệu ứng này sẽ chiếm ưu thế. Sự phụ thuộc dữ liệu của thuật toán của bạn đóng một vai trò quan trọng ở đây, do đó, không có so sánh chung nào sẽ dự đoán khoảng cách hiệu suất trong vấn đề của bạn.

Ví dụ: phép cộng số nguyên có tính chất giao hoán, vì vậy nếu trình biên dịch thấy một vòng lặp giống như bạn đã sử dụng cho điểm chuẩn (giả sử dữ liệu ngẫu nhiên đã được chuẩn bị trước để nó không che khuất kết quả), nó có thể mở vòng lặp và tính tổng một phần bằng không có phụ thuộc, sau đó thêm chúng khi vòng lặp kết thúc. Nhưng với dấu phẩy động, trình biên dịch phải thực hiện các hoạt động theo cùng thứ tự bạn yêu cầu (bạn đã có các điểm trình tự trong đó nên trình biên dịch phải đảm bảo cùng một kết quả, điều này không cho phép sắp xếp lại thứ tự) vì vậy có sự phụ thuộc mạnh mẽ của mỗi phép bổ sung vào kết quả của cái trước.

Bạn cũng có thể phù hợp với nhiều toán hạng số nguyên hơn trong bộ nhớ cache tại một thời điểm. Vì vậy, phiên bản điểm cố định có thể hoạt động tốt hơn phiên bản float theo thứ tự độ lớn ngay cả trên một máy mà FPU có thông lượng cao hơn về mặt lý thuyết.


4
+1 để chỉ ra cách các điểm chuẩn ngây thơ có thể mang lại vòng lặp 0 lần do các phép toán số nguyên không đổi không được cuộn. Hơn nữa, trình biên dịch hoàn toàn có thể loại bỏ vòng lặp (số nguyên hoặc FP) nếu kết quả không thực sự được sử dụng.
vladr

Kết luận cho điều đó là: người ta phải gọi một hàm có biến lặp làm đối số. Vì tôi nghĩ rằng không có trình biên dịch nào có thể thấy rằng hàm không làm gì cả và lệnh gọi có thể bị bỏ qua. Vì có tổng chi phí cuộc gọi, chỉ có sự khác biệt về thời gian == (thời gian thực - thời gian nguyên) là đáng kể.
GameAlchemist

@GameAlchemist: Nhiều trình biên dịch loại bỏ các lệnh gọi đến các hàm trống, như một tác dụng phụ của nội tuyến. Bạn phải nỗ lực để ngăn chặn điều đó.
Ben Voigt

OP có vẻ như đang nói về việc sử dụng số nguyên cho những thứ mà FP sẽ phù hợp tự nhiên hơn, vì vậy sẽ cần nhiều mã số nguyên hơn để đạt được kết quả giống như mã FP. Trong trường hợp này, chỉ cần sử dụng FP. Ví dụ: trên phần cứng có FPU (ví dụ: CPU máy tính để bàn), bộ giải mã MP3 số nguyên điểm cố định chậm hơn (và lỗi làm tròn nhiều hơn một chút) so với bộ giải mã dấu chấm động. Việc triển khai điểm cố định của codec chủ yếu tồn tại để chạy trên các CPU ARM rút gọn không có phần cứng FP, chỉ FP giả lập chậm.
Peter Cordes

một ví dụ cho các điểm đầu tiên: trên x86-64 với AVX-512 có được chỉ có 16 thanh ghi GP nhưng 32 thanh ghi ZMM nên vô hướng floating-point toán có thể được nhanh hơn
phuclv

18

Phép cộng nhanh hơn nhiều rand, vì vậy chương trình của bạn (đặc biệt) vô dụng.

Bạn cần xác định các điểm nóng về hiệu suất và từng bước sửa đổi chương trình của mình. Có vẻ như bạn có vấn đề với môi trường phát triển của mình và cần được giải quyết trước. Không thể chạy chương trình của bạn trên PC vì một vấn đề nhỏ được đặt ra?

Nói chung, cố gắng thực hiện các công việc FP với số học nguyên là một công thức dẫn đến chậm.


Vâng, cũng như việc chuyển đổi từ số nguyên rand thành số float trong phiên bản dấu phẩy động. Bất kỳ ý tưởng về một cách tốt hơn để kiểm tra điều này?
maxpenguin

1
Nếu bạn đang cố gắng điều chỉnh tốc độ, hãy xem POSIX timespec_thoặc thứ gì đó tương tự. Ghi lại thời gian bắt đầu và kết thúc vòng lặp và lấy chênh lệch. Sau đó di chuyển việc randtạo dữ liệu ra khỏi vòng lặp. Đảm bảo rằng thuật toán của bạn lấy tất cả dữ liệu của nó từ các mảng và đặt tất cả dữ liệu của nó vào các mảng. Điều đó có được thuật toán thực tế của bạn và được thiết lập, xử lý dữ liệu, in kết quả, mọi thứ trừ chuyển đổi tác vụ và ngắt khỏi vòng lặp hồ sơ của bạn.
Mike D.

3
@maxpenguin: câu hỏi là bạn đang thử nghiệm cái gì. Artem đã cho rằng bạn đang làm đồ họa, Carl đã xem xét liệu bạn có đang sử dụng nền tảng nhúng không có FP, tôi cho rằng bạn đang viết mã khoa học cho một máy chủ. Bạn không thể khái quát hóa hoặc "viết" các điểm chuẩn. Điểm chuẩn được lấy mẫu từ công việc thực tế mà chương trình của bạn thực hiện. Một điều tôi có thể nói với bạn là nó sẽ không vẫn "về cơ bản cùng một tốc độ" nếu bạn chạm vào yếu tố quan trọng về hiệu suất trong chương trình của mình, bất kể đó là gì.
Potatoswatter

điểm tốt và câu trả lời tốt. Chúng tôi có một cơ sở mã rộng rãi hiện tại. Thực sự tôi đã đưa ra chống lại sự tổng quát hóa rằng chúng ta "không được sử dụng float vì phép tính số nguyên nhanh hơn" - và tôi đang tìm cách (nếu điều này thậm chí đúng) để bác bỏ giả thiết tổng quát này. Tôi nhận ra rằng sẽ không thể dự đoán chính xác kết quả đối với chúng tôi nếu không thực hiện tất cả công việc và lập hồ sơ sau đó. Dù sao, cảm ơn sự giúp đỡ của bạn.
maxpenguin

18

TIL Điều này thay đổi (rất nhiều). Dưới đây là một số kết quả bằng cách sử dụng trình biên dịch gnu (btw tôi cũng đã kiểm tra bằng cách biên dịch trên máy, gnu g ++ 5.4 từ xenial nhanh hơn rất nhiều so với 4.6.3 từ linaro về chính xác)

Intel i7 4700MQ xenial

short add: 0.822491
short sub: 0.832757
short mul: 1.007533
short div: 3.459642
long add: 0.824088
long sub: 0.867495
long mul: 1.017164
long div: 5.662498
long long add: 0.873705
long long sub: 0.873177
long long mul: 1.019648
long long div: 5.657374
float add: 1.137084
float sub: 1.140690
float mul: 1.410767
float div: 2.093982
double add: 1.139156
double sub: 1.146221
double mul: 1.405541
double div: 2.093173

Intel i3 2370M cũng có kết quả tương tự

short add: 1.369983
short sub: 1.235122
short mul: 1.345993
short div: 4.198790
long add: 1.224552
long sub: 1.223314
long mul: 1.346309
long div: 7.275912
long long add: 1.235526
long long sub: 1.223865
long long mul: 1.346409
long long div: 7.271491
float add: 1.507352
float sub: 1.506573
float mul: 2.006751
float div: 2.762262
double add: 1.507561
double sub: 1.506817
double mul: 1.843164
double div: 2.877484

Intel (R) Celeron (R) 2955U (Acer C720 Chromebook chạy xenial)

short add: 1.999639
short sub: 1.919501
short mul: 2.292759
short div: 7.801453
long add: 1.987842
long sub: 1.933746
long mul: 2.292715
long div: 12.797286
long long add: 1.920429
long long sub: 1.987339
long long mul: 2.292952
long long div: 12.795385
float add: 2.580141
float sub: 2.579344
float mul: 3.152459
float div: 4.716983
double add: 2.579279
double sub: 2.579290
double mul: 3.152649
double div: 4.691226

DigitalOcean 1GB Droplet CPU Intel (R) Xeon (R) E5-2630L v2 (chạy đáng tin cậy)

short add: 1.094323
short sub: 1.095886
short mul: 1.356369
short div: 4.256722
long add: 1.111328
long sub: 1.079420
long mul: 1.356105
long div: 7.422517
long long add: 1.057854
long long sub: 1.099414
long long mul: 1.368913
long long div: 7.424180
float add: 1.516550
float sub: 1.544005
float mul: 1.879592
float div: 2.798318
double add: 1.534624
double sub: 1.533405
double mul: 1.866442
double div: 2.777649

Bộ xử lý AMD Opteron (tm) 4122 (chính xác)

short add: 3.396932
short sub: 3.530665
short mul: 3.524118
short div: 15.226630
long add: 3.522978
long sub: 3.439746
long mul: 5.051004
long div: 15.125845
long long add: 4.008773
long long sub: 4.138124
long long mul: 5.090263
long long div: 14.769520
float add: 6.357209
float sub: 6.393084
float mul: 6.303037
float div: 17.541792
double add: 6.415921
double sub: 6.342832
double mul: 6.321899
double div: 15.362536

Điều này sử dụng mã từ http://pastebin.com/Kx8WGUfg nhưbenchmark-pc.c

g++ -fpermissive -O3 -o benchmark-pc benchmark-pc.c

Tôi đã chạy nhiều lần, nhưng có vẻ như đây là trường hợp các con số chung đều giống nhau.

Một ngoại lệ đáng chú ý dường như là ALU mul vs FPU mul. Phép cộng và phép trừ có vẻ khác nhau một cách đáng kể.

Đây là ở trên ở dạng biểu đồ (nhấp vào để xem kích thước đầy đủ, thấp hơn là nhanh hơn và thích hợp hơn):

Biểu đồ dữ liệu trên

Cập nhật lên accomodate @Peter Cordes

https://gist.github.com/Lewiscowles1986/90191c59c9aedf3d08bf0b129065cccc

i7 4700MQ Linux Ubuntu Xenial 64-bit (áp dụng tất cả các bản vá cho 2018-03-13)
    short add: 0.773049
    short sub: 0.789793
    short mul: 0.960152
    short div: 3.273668
      int add: 0.837695
      int sub: 0.804066
      int mul: 0.960840
      int div: 3.281113
     long add: 0.829946
     long sub: 0.829168
     long mul: 0.960717
     long div: 5.363420
long long add: 0.828654
long long sub: 0.805897
long long mul: 0.964164
long long div: 5.359342
    float add: 1.081649
    float sub: 1.080351
    float mul: 1.323401
    float div: 1.984582
   double add: 1.081079
   double sub: 1.082572
   double mul: 1.323857
   double div: 1.968488
Bộ xử lý AMD Opteron (tm) 4122 (chính xác, lưu trữ chia sẻ DreamHost)
    short add: 1.235603
    short sub: 1.235017
    short mul: 1.280661
    short div: 5.535520
      int add: 1.233110
      int sub: 1.232561
      int mul: 1.280593
      int div: 5.350998
     long add: 1.281022
     long sub: 1.251045
     long mul: 1.834241
     long div: 5.350325
long long add: 1.279738
long long sub: 1.249189
long long mul: 1.841852
long long div: 5.351960
    float add: 2.307852
    float sub: 2.305122
    float mul: 2.298346
    float div: 4.833562
   double add: 2.305454
   double sub: 2.307195
   double mul: 2.302797
   double div: 5.485736
Intel Xeon E5-2630L v2 @ 2.4GHz (Trusty 64-bit, DigitalOcean VPS)
    short add: 1.040745
    short sub: 0.998255
    short mul: 1.240751
    short div: 3.900671
      int add: 1.054430
      int sub: 1.000328
      int mul: 1.250496
      int div: 3.904415
     long add: 0.995786
     long sub: 1.021743
     long mul: 1.335557
     long div: 7.693886
long long add: 1.139643
long long sub: 1.103039
long long mul: 1.409939
long long div: 7.652080
    float add: 1.572640
    float sub: 1.532714
    float mul: 1.864489
    float div: 2.825330
   double add: 1.535827
   double sub: 1.535055
   double mul: 1.881584
   double div: 2.777245

gcc5 có thể tự động vectơ hóa một cái gì đó mà gcc4.6 không? Được benchmark-pcđo lường một số sự kết hợp của thông lượng và độ trễ? Trên Haswell (i7 4700MQ) của bạn, nhân số nguyên là 1 cho mỗi thông lượng xung nhịp, độ trễ 3 chu kỳ, nhưng thêm / phụ số nguyên là 4 cho mỗi thông lượng đồng hồ, độ trễ 1 chu kỳ ( agner.org/optimize ). Vì vậy, có lẽ có rất nhiều chi phí vòng lặp làm loãng những con số đó cho add và mul xuất hiện quá gần (thêm dài: 0,824088 so với dài mul: 1,017164). (gcc mặc định không hủy cuộn vòng lặp, ngoại trừ việc hủy cuộn hoàn toàn số lần lặp lại rất thấp).
Peter Cordes

Và BTW, tại sao nó không kiểm tra int, chỉ shortlong? Trên Linux x86-64, shortlà 16 bit (và do đó có làm chậm thanh ghi một phần trong một số trường hợp), trong khi longlong longcả hai đều là loại 64 bit. (Có thể nó được thiết kế cho Windows mà x86-64 vẫn sử dụng 32 bit long? Hoặc có thể nó được thiết kế cho chế độ 32 bit.) Trên Linux, x32 ABI có 32 bit longở chế độ 64 bit , vì vậy nếu bạn đã cài đặt các thư viện , sử dụng gcc -mx32để biên dịch cho ILP32. Hoặc chỉ cần sử dụng -m32và nhìn vào các longcon số.
Peter Cordes

Và bạn thực sự nên kiểm tra xem trình biên dịch của bạn có tự động vector hóa bất kỳ thứ gì hay không. ví dụ: sử dụng addpstrên thanh ghi xmm thay vì addss, để thực hiện 4 FP sẽ thêm song song trong một lệnh nhanh như vô hướng addss. (Sử dụng -march=nativeđể cho phép sử dụng bất kỳ bộ lệnh nào mà CPU của bạn hỗ trợ, không chỉ đường cơ sở SSE2 cho x86-64).
Peter Cordes

@cincodenada vui lòng để lại các biểu đồ hiển thị đầy đủ 15 ở bên cạnh vì nó là minh họa về hiệu suất.
MrMesees

@PeterCordes Tôi sẽ cố gắng xem xét vào ngày mai, cảm ơn bạn đã siêng năng.
MrMesees

7

Hai điểm cần xem xét -

Phần cứng hiện đại có thể chồng chéo các hướng dẫn, thực thi chúng song song và sắp xếp lại thứ tự để sử dụng phần cứng tốt nhất. Ngoài ra, bất kỳ chương trình dấu phẩy động quan trọng nào cũng có khả năng làm việc với số nguyên đáng kể ngay cả khi nó chỉ tính toán các chỉ số thành mảng, bộ đếm vòng lặp, v.v. vì vậy ngay cả khi bạn có lệnh dấu phẩy động chậm, nó cũng có thể đang chạy trên một phần cứng riêng biệt chồng chéo với một số công việc số nguyên. Quan điểm của tôi là ngay cả khi các lệnh dấu phẩy động chậm hơn các số nguyên, chương trình tổng thể của bạn có thể chạy nhanh hơn vì nó có thể sử dụng nhiều phần cứng hơn.

Như mọi khi, cách duy nhất để chắc chắn là lập hồ sơ chương trình thực tế của bạn.

Điểm thứ hai là hầu hết các CPU ngày nay đều có hướng dẫn SIMD cho dấu phẩy động có thể hoạt động trên nhiều giá trị dấu phẩy động cùng một lúc. Ví dụ, bạn có thể tải 4 phao vào một thanh ghi SSE và thực hiện 4 phép nhân trên tất cả chúng song song. Nếu bạn có thể viết lại các phần mã của mình để sử dụng hướng dẫn SSE thì có vẻ như nó sẽ nhanh hơn phiên bản số nguyên. Visual c ++ cung cấp các chức năng nội tại của trình biên dịch để thực hiện việc này, hãy xem http://msdn.microsoft.com/en-us/library/x5c07e2a(v=VS.80).aspx để biết một số thông tin.


Cần lưu ý rằng trên Win64, các hướng dẫn FPU không được tạo bởi trình biên dịch MSVC nữa. Dấu chấm động luôn sử dụng hướng dẫn SIMD ở đó. Điều này tạo ra sự khác biệt lớn về tốc độ giữa Win32 và Win64 liên quan đến lỗi.
James Dunne

5

Phiên bản dấu phẩy động sẽ chậm hơn nhiều, nếu không có hoạt động còn lại. Vì tất cả các phép cộng là tuần tự, nên cpu sẽ không thể thực hiện song song phép tính tổng. Độ trễ sẽ rất quan trọng. Độ trễ thêm FPU thường là 3 chu kỳ, trong khi thêm số nguyên là 1 chu kỳ. Tuy nhiên, bộ chia cho toán tử còn lại có thể sẽ là phần quan trọng, vì nó không được kết nối hoàn toàn trên cpu hiện đại. vì vậy, giả sử lệnh chia / phần dư sẽ tiêu tốn phần lớn thời gian, sự khác biệt do thêm độ trễ sẽ nhỏ.


4

Trừ khi bạn đang viết mã sẽ được gọi hàng triệu lần mỗi giây (chẳng hạn như vẽ một đường thẳng lên màn hình trong ứng dụng đồ họa), số nguyên so với số học dấu phẩy động hiếm khi là nút thắt cổ chai.

Bước đầu tiên thông thường đối với các câu hỏi về hiệu quả là lập hồ sơ mã của bạn để xem thời gian chạy thực sự được sử dụng ở đâu. Lệnh linux cho điều này là gprof.

Biên tập:

Mặc dù tôi cho rằng bạn luôn có thể triển khai thuật toán vẽ đoạn thẳng bằng cách sử dụng số nguyên và số dấu phẩy động, hãy gọi nó là một số lượng lớn và xem nó có tạo ra sự khác biệt hay không:

http://en.wikipedia.org/wiki/Bresenham's_algorithm


2
Các ứng dụng khoa học sử dụng FP. Ưu điểm duy nhất của FP là độ chính xác không thay đổi theo tỷ lệ. Nó giống như ký hiệu khoa học. Nếu bạn đã biết quy mô của các con số (ví dụ: độ dài dòng là một số pixel), FP sẽ bị loại bỏ. Nhưng trước khi bạn vẽ đường thẳng, điều đó không đúng.
Potatoswatter

4

Ngày nay, các phép toán số nguyên thường nhanh hơn một chút so với các phép toán dấu phẩy động. Vì vậy, nếu bạn có thể thực hiện một phép tính với các phép toán tương tự trong số nguyên và dấu phẩy động, hãy sử dụng số nguyên. TUY NHIÊN bạn đang nói "Điều này gây ra rất nhiều vấn đề khó chịu và thêm rất nhiều mã khó chịu". Điều đó có vẻ như bạn cần nhiều phép toán hơn vì bạn sử dụng số học nguyên thay vì dấu phẩy động. Trong trường hợp đó, dấu chấm động sẽ chạy nhanh hơn vì

  • Ngay sau khi bạn cần nhiều phép toán số nguyên hơn, bạn có thể cần nhiều hơn nữa, vì vậy lợi thế về tốc độ nhẹ hơn bị ăn mòn bởi các phép toán bổ sung

  • mã dấu phẩy động đơn giản hơn, có nghĩa là viết mã nhanh hơn, có nghĩa là nếu tốc độ là quan trọng, bạn có thể dành nhiều thời gian hơn để tối ưu hóa mã.


Có rất nhiều suy đoán hoang dã ở đây, không tính đến bất kỳ hiệu ứng phụ nào có trong phần cứng, thường chi phối thời gian tính toán. Một điểm khởi đầu không tồi, nhưng nó cần được kiểm tra trên từng ứng dụng cụ thể thông qua việc lập hồ sơ, và không được dạy như phúc âm.
Ben Voigt

3

Tôi đã chạy một bài kiểm tra chỉ thêm 1 vào số thay vì rand (). Kết quả (trên x86-64) là:

  • ngắn: 4,260 giây
  • int: 4.020 giây
  • dài dài: 3,350 giây
  • float: 7.330 giây
  • gấp đôi: 7.210 giây

1
Nguồn, tùy chọn biên dịch và phương pháp định thời gian? Tôi hơi ngạc nhiên về kết quả.
GManNickG

Vòng lặp tương tự như OP với "rand ()% 365" được thay thế bằng "1". Không có tối ưu hóa. Thời gian của người dùng từ lệnh "time".
dan04

13
"Không có tối ưu hóa" là chìa khóa. Bạn không bao giờ lập hồ sơ khi tắt tối ưu hóa, luôn lập hồ sơ ở chế độ "phát hành".
Dean Harding

2
Tuy nhiên, trong trường hợp này, việc tắt tối ưu hóa buộc op phải xảy ra và được thực hiện có chủ ý - vòng lặp ở đó để giãn thời gian đến một thang đo hợp lý. Sử dụng hằng số 1 loại bỏ chi phí của rand (). Một trình biên dịch tối ưu hóa đủ thông minh sẽ thấy 1 được thêm 100.000.000 lần mà không có cách nào thoát khỏi vòng lặp và chỉ cần thêm 100000000 trong một lần chọn. Loại đó xoay quanh toàn bộ mục đích, phải không?
Stan Rogers

7
@Stan, làm cho biến dễ bay hơi. Ngay cả một trình biên dịch tối ưu hóa thông minh cũng nên thực hiện nhiều hoạt động sau đó.
vladr

0

Dựa trên "điều gì đó tôi đã nghe" rất đáng tin cậy, ngày xưa, phép tính số nguyên nhanh hơn khoảng 20 đến 50 lần dấu phẩy động và ngày nay nó nhanh hơn ít hơn hai lần.


1
Vui lòng xem xét nhìn vào này một lần nữa cung cấp hơn ý kiến (đặc biệt là cho rằng quan điểm dường như bay khi đối mặt với thực tế thu thập được)
MrMesees

1
@MrMesees Mặc dù câu trả lời này không hữu ích lắm nhưng tôi muốn nói rằng nó phù hợp với các thử nghiệm bạn đã thực hiện. Và câu đố lịch sử có lẽ cũng tốt.
Jonatan Öström

Là một người đã làm việc với 286s hồi trước, tôi có thể khẳng định rằng; "CÓ ... họ đã!"
David H Parry
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.