النقطة العائمة مقابل حسابات عدد صحيح على الأجهزة الحديثة

StackOverflow https://stackoverflow.com/questions/2550281

سؤال

أقوم ببعض الأعمال الحاسمة في الأداء في C ++ ، ونحن نستخدم حاليًا حسابات عدد صحيح للمشاكل التي تطفو بطبيعتها لأن "أسرع". هذا يسبب الكثير من المشاكل المزعجة ويضيف الكثير من التعليمات البرمجية المزعجة.

الآن ، أتذكر أنني قرأت كيف كانت حسابات النقطة العائمة بطيئة للغاية تقريبًا حوالي 386 يومًا ، حيث أعتقد (IIRC) أن هناك مجموعة مشتركة اختيارية. ولكن بالتأكيد في الوقت الحاضر مع وحدات المعالجة المركزية الأكثر تعقيدًا وقوة ، لا تحدث فرقًا في "السرعة" إذا كان القيام بالنقطة العائمة أو حساب عدد صحيح؟ خاصة وأن وقت الحساب الفعلي يكون صغيرًا مقارنة بشيء مثل التسبب في كشك خط أنابيب أو جلب شيء ما من الذاكرة الرئيسية؟

أعلم أن الإجابة الصحيحة هي القياس على الأجهزة المستهدفة ، ما هي طريقة جيدة لاختبار هذا؟ لقد كتبت برنامجين صغيرين C ++ وقارن وقت التشغيل مع "الوقت" على Linux ، لكن وقت التشغيل الفعلي متغير للغاية (لا يساعدني على تشغيل خادم افتراضي). أقل من قضاء يومي بأكمله في تشغيل مئات المعايير ، وصنع الرسوم البيانية وما إلى ذلك ، هل هناك شيء يمكنني القيام به للحصول على اختبار معقول للسرعة النسبية؟ أي أفكار أو أفكار؟ هل أنا مخطئ تماما؟

البرامج التي استخدمتها على النحو التالي ، فهي ليست متطابقة بأي وسيلة:

#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;
}

البرنامج 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;
}

شكرا لك مقدما!

تحرير: المنصة التي أهتم بها هي X86 أو x86-64 العادية التي تعمل على أجهزة سطح المكتب و Windows.

تحرير 2 (تم لصقه من تعليق أدناه): لدينا قاعدة رمز واسعة حاليًا. لقد واجهت حقًا التعميم القائل بأنه "يجب ألا نستخدم التعويم لأن حساب عدد صحيح أسرع" - وأنا أبحث عن طريقة (إذا كان هذا صحيحًا) لدحض هذا الافتراض المعمم. أدرك أنه سيكون من المستحيل التنبؤ بالنتيجة الدقيقة لنا دون القيام بكل العمل والتوصيف بعد ذلك.

على أي حال ، شكرًا على كل إجاباتك ومساعداتك الممتازة. لا تتردد في إضافة أي شيء آخر :).

هل كانت مفيدة؟

المحلول

للأسف ، لا يمكنني إلا أن أعطيك إجابة "يعتمد" ...

من تجربتي ، هناك العديد من المتغيرات إلى الأداء ... خاصة بين عدد صحيح واتفاقيات النقطة العائمة. يختلف اختلافًا كبيرًا من المعالج إلى المعالج (حتى داخل نفس العائلة مثل X86) لأن المعالجات المختلفة لها أطوال "خط أنابيب" مختلف. وأيضًا ، تكون بعض العمليات بسيطة للغاية (مثل الإضافة) ولها طريق متسارع عبر المعالج ، والبعض الآخر (مثل التقسيم) يستغرق وقتًا أطول بكثير.

المتغير الكبير الآخر هو المكان الذي توجد فيه البيانات. إذا كان لديك بعض القيم التي يجب إضافتها فقط ، فيمكن أن تكون جميع البيانات موجودة في ذاكرة التخزين المؤقت ، حيث يمكن إرسالها بسرعة إلى وحدة المعالجة المركزية. ستكون عملية النقطة العائمة البطيئة جدًا التي تحتوي بالفعل على البيانات الموجودة في ذاكرة التخزين المؤقت أسرع عدة مرات من عملية عدد صحيح حيث يحتاج عدد صحيح إلى نسخ من ذاكرة النظام.

أفترض أنك تطرح هذا السؤال لأنك تعمل على تطبيق مهم للأداء. إذا كنت تتطور للهندسة المعمارية X86 ، وكنت بحاجة إلى أداء إضافي ، فقد ترغب في النظر في استخدام ملحقات SSE. يمكن أن يؤدي ذلك إلى تسريع إلى حد كبير من الحساب العائم أحادي الدقة ، حيث يمكن إجراء نفس العملية على بيانات متعددة في وقت واحد ، بالإضافة إلى وجود ضفة منفصلة من السجلات لعمليات SSE. (لقد لاحظت في مثالك الثاني أنك استخدمت "Float" بدلاً من "Double" ، مما يجعلني أعتقد أنك تستخدم الرياضيات ذات الدقة الواحدة).

*ملاحظة: سيؤدي استخدام تعليمات MMX القديمة إلى إبطاء البرامج بالفعل ، لأن تلك التعليمات القديمة استخدمت بالفعل نفس سجلات FPU ، مما يجعل من المستحيل استخدام كل من FPU و MMX في نفس الوقت.

نصائح أخرى

على سبيل المثال (أرقام أقل أسرع) ،

64 بت Intel Xeon X5550 @ 2.67 جيجا هرتز ، 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]

32 بت مزدوج AMD Opteron (TM) معالج 265 @ 1.81 جيجا هرتز ، 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]

مثل أشار دان, ، حتى بمجرد تطبيع تردد الساعة (والذي يمكن أن يكون مضللاً في حد ذاته في تصاميم أنابيب) ، ستختلف النتائج بشكل كبير بناءً على بنية وحدة المعالجة المركزية (فرد ألو/FPU أداء, إلى جانب فِعلي عدد alus/fpus متاح لكل قلب في superscalar التصميمات التي تؤثر على عدد يمكن أن تنفذ العمليات المستقلة بالتوازي - لا يتم ممارسة العامل الأخير بواسطة الكود أدناه لأن جميع العمليات أدناه تعتمد بشكل متتابع.)

فقير الرجل الفقراء FPU/ALU المعيار:

#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;
}

من المحتمل أن يكون هناك اختلاف كبير في سرعة العالم الحقيقي بين الرياضيات ذات النقطة الثابتة والرياضيات العائمة ، ولكن الإنتاجية النظرية الأفضل في ALU vs FPU غير ذي صلة تمامًا. بدلاً من ذلك ، فإن عدد سجلات عدد صحيح ونقاط عائمة (سجلات حقيقية ، وليس تسجيل أسماء) على بنيةك التي لا تستخدمها خلاف ذلك عن طريق حسابك (على سبيل المثال للتحكم في الحلقة) ، وعدد العناصر من كل نوع يتناسب مع خط ذاكرة التخزين المؤقت ، التحسينات الممكنة بالنظر إلى الدلالات المختلفة للرياضيات العائمة مقابل النقطة العائمة - ستهيمن هذه الآثار. تلعب تبعيات البيانات في الخوارزمية دورًا مهمًا هنا ، بحيث لا تتنبأ أي مقارنة عامة بفجوة الأداء في مشكلتك.

على سبيل المثال ، تكون إضافة عدد صحيح تبادلًا ، لذلك إذا رأى المترجم حلقة مثلك تستخدم لمعيار (على افتراض أن البيانات العشوائية تم إعدادها مسبقًا حتى لا تحجب النتائج) ، فيمكنها إلغاء حلقة وحساب المبالغ الجزئية مع لا تبعيات ، ثم أضفها عندما تنتهي الحلقة. ولكن مع النقطة العائمة ، يتعين على المترجم القيام بالعمليات بنفس الترتيب الذي طلبته (لديك نقاط تسلسل هناك بحيث يتعين على المترجم أن يضمن نفس النتيجة ، والتي لا تسمح بترتيبها) لذلك هناك اعتماد قوي لكل إضافة على نتيجة السابقة.

من المحتمل أن تتناسب مع المزيد من المعاملات الصحيحة في ذاكرة التخزين المؤقت في وقت واحد أيضًا. وبالتالي ، قد يتفوق الإصدار الثابت على إصدار Float بترتيب من حيث الحجم حتى على جهاز يكون فيه FPU إنتاجية أعلى نظريًا.

الإضافة أسرع بكثير من rand, ، لذلك برنامجك (خاصة) عديمة الفائدة.

تحتاج إلى تحديد النقاط الساخنة للأداء وتعديل البرنامج بشكل متزايد. يبدو أن لديك مشاكل في بيئة التطوير الخاصة بك والتي ستحتاج إلى حلها أولاً. هل من المستحيل تشغيل برنامجك على جهاز الكمبيوتر الخاص بك للحصول على مشكلة صغيرة؟

بشكل عام ، فإن محاولة وظائف FP مع الحساب الصحيح هي وصفة للبطء.

حتى هذا يختلف (كثيرا). فيما يلي بعض النتائج باستخدام برنامج التحويل البرمجي GNU (راجع للشغل ، قمت أيضًا بالتحقق من خلال التجميع على الآلات ، GNU G ++ 5.4 من Xenial هو جحيم أسرع بكثير من 4.6.3 من Linaro على دقيق)

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 لديها نتائج مماثلة

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 Running 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 Intel (R) Xeon (R) CPU E5-2630L V2 (تشغيل ثقة)

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

AMD Opteron (TM) معالج 4122 (دقيق)

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

هذا يستخدم التعليمات البرمجية من http://pastebin.com/kx8wgufg مثل benchmark-pc.c

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

لقد قمت بتشغيل تمريرات متعددة ، ولكن يبدو أن هذا هو الحال أن الأرقام العامة هي نفسها.

يبدو أن أحد الاستثناءات البارزة هو alu mul vs fpu mul. الإضافة والطرح يبدو مختلفا بشكل تافهة.

إليك ما سبق في نموذج الرسم البياني (انقر للحصول على حجم كامل ، أقل أسرع وفضل):

Chart of above data

تحديث لإقامة كوردس peter

https://gist.github.com/lewiscowles1986/90191C59C9AEDF3D08BF0B129065CCCC

i7 4700mq Linux Ubuntu Xenial 64 bit (جميع التصحيحات إلى 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
AMD Opteron (TM) معالج 4122 (دقيق ، 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.4 جيجا هرتز (موثوق 64 بت ، 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

نقطتان للنظر -

يمكن للأجهزة الحديثة تداخل الإرشادات ، وتنفيذها بالتوازي وإعادة ترتيبها لاستخدام الأجهزة بشكل أفضل. وأيضًا ، من المرجح أن يكون لأي برنامج نقطي عائم مهم عمل عددًا صحيحًا أيضًا حتى لو كان يحسب فقط المؤشرات في صفائف ، عداد الحلقة وما إلى ذلك ، لذا حتى لو كان لديك تعليمات بطيئة في النقطة العائمة ، فقد يتم تشغيلها على جزء منفصل من الأجهزة تداخل مع بعض أعمال الصدفة. نقطتي هي أنه حتى لو كانت تعليمات النقطة العائمة بطيئة تلك التي يتمتع بها عدد صحيح ، فقد يعمل برنامجك العام بشكل أسرع لأنه يمكن أن يستفيد من المزيد من الأجهزة.

كما هو الحال دائمًا ، فإن الطريقة الوحيدة للتأكد من أن برنامجك الفعلي.

النقطة الثانية هي أن معظم وحدات المعالجة المركزية في هذه الأيام لديها تعليمات SIMD للنقطة العائمة التي يمكن أن تعمل على قيم نقطة عائمة متعددة في نفس الوقت. على سبيل المثال ، يمكنك تحميل 4 عوامات في سجل SSE واحد وأداء 4 مضاعفات عليها جميعًا بالتوازي. إذا تمكنت من إعادة كتابة أجزاء من التعليمات البرمجية الخاصة بك لاستخدام تعليمات SSE ، فيبين من المحتمل أن تكون أسرع من إصدار عدد صحيح. يوفر Visual C ++ وظائف جوهرية للمترجم للقيام بذلك ، انظر http://msdn.microsoft.com/en-us/library/x5c07e2a(v=vs.80).aspx لبعض المعلومات.

ما لم تكن تكتب التعليمات البرمجية التي سيتم تسميتها ملايين المرات في الثانية (مثل ، على سبيل المثال ، فإن رسم خط إلى الشاشة في تطبيق الرسومات) ، نادراً ما يكون الحساب الصحيح مقابل النقطة العائمة عنق الزجاجة.

تتمثل الخطوة الأولى المعتادة في أسئلة الكفاءة في تعريف الكود الخاص بك لمعرفة أين يتم إنفاق وقت التشغيل بالفعل. أمر Linux لهذا هو gprof.

تعديل:

على الرغم من أنني أظن أنه يمكنك دائمًا تطبيق خوارزمية رسم الخط باستخدام الأعداد الصحيحة وأرقام الفاصلة العائمة ، إلا أن تسميها عددًا كبيرًا من المرات ومعرفة ما إذا كان يحدث فرقًا:

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

ستكون نسخة النقطة العائمة أبطأ بكثير ، إذا لم يكن هناك عملية ما تبقى. نظرًا لأن جميع الإضافات متتابعة ، فلن تتمكن وحدة المعالجة المركزية من موازاة التجميع. سيكون الكمون أمرًا بالغ الأهمية. FPU إضافة الكمون هو عادة 3 دورات ، في حين أن إضافة عدد صحيح هو دورة واحدة. ومع ذلك ، فإن المقسم للمشغل الباقي على الأرجح هو الجزء الحاسم ، لأنه لم يتم تحديد أنابيب بالكامل على وحدة المعالجة المركزية الحديثة. لذلك ، على افتراض أن تعليمات الفجوة/الباقي سوف تستهلك الجزء الأكبر من الوقت ، فإن الفرق الناتج عن إضافة الكمون سيكون صغيرًا.

اليوم ، عادة ما تكون عمليات عدد صحيح أسرع قليلاً من عمليات النقطة العائمة. لذلك إذا كنت تستطيع إجراء حساب مع نفس العمليات في عدد صحيح ونقطة عائمة ، فاستخدم عدد صحيح. ومع ذلك ، فأنت تقول "هذا يسبب الكثير من المشكلات المزعجة ويضيف الكثير من التعليمات البرمجية المزعجة". هذا يبدو أنك بحاجة إلى مزيد من العمليات لأنك تستخدم حسابًا صحيحًا بدلاً من النقطة العائمة. في هذه الحالة ، ستعمل النقطة العائمة بشكل أسرع بسبب

  • بمجرد أن تحتاج إلى المزيد من عمليات عدد صحيح ، ربما تحتاج إلى الكثير ، وبالتالي فإن ميزة السرعة الطفيفة أكثر من تناولها من قبل العمليات الإضافية

  • رمز النقطة العائمة أبسط ، مما يعني أنه من الأسرع كتابة الرمز ، مما يعني أنه إذا كانت السرعة حاسمة ، فيمكنك قضاء المزيد من الوقت في تحسين الكود.

أجريت اختبارًا أضاف للتو 1 إلى الرقم بدلاً من Rand (). النتائج (في x86-64) كانت:

  • قصير: 4.260s
  • int: 4.020s
  • طويل طويل: 3.350s
  • تعويم: 7.330s
  • مزدوج: 7.210s

استنادًا إلى هذا "شيء سمعته" موثوق به ، في الأيام الخوالي ، كان حساب عدد صحيح أسرع حوالي 20 إلى 50 مرة من تلك النقطة العائمة ، وفي هذه الأيام أقل من ضعف أسرع.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top