سؤال

كنت أقوم بتصحيح مشروعي ولم أتمكن من العثور على خطأ. أخيرا لقد حددت ذلك. انظر إلى الكود. تعتقد أن كل شيء على ما يرام ، وستكون النتيجة "حسنًا! حسنًا! حسنًا!" ، أليس كذلك؟ الآن قم بتجميعها مع VC (لقد جربت VS2005 و VS2008).

#include <math.h>
#include <stdio.h>


int main () {
    for ( double x = 90100.0; x<90120.0; x+=1 )
    {
        if ( cos(x) == cos(x) )
            printf ("x==%f  OK!\n", x);
        else
            printf ("x==%f  FAIL!\n", x);
    }

    getchar();
    return 0; 
}

الثابت السحري المزدوج هو 90112.0. عندما يكون x <90112.0 كل شيء على ما يرام ، عندما x> 90112.0 - كلا! يمكنك تغيير cos إلى الخطيئة.

أيه أفكار؟ لا تنسى أن الخطيئة و cos دورية.

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

المحلول

يمكن أن يكون هذا: http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.18

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

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

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

نصائح أخرى

كما لاحظ آخرون ، تقوم مكتبة VS Math بإجراء حسابها على X87 FPU ، وتوليد نتائج 80 بت على الرغم من أن النوع مضاعف.

هكذا:

  1. يتم استدعاء COS () ، ويعود مع COS (X) في الجزء العلوي من مكدس X87 كتعويم 80bit
  2. يتم عرض COS (X) من مكدس x87 وتخزينه على الذاكرة كمضاعفة ؛ هذا يتسبب في تقريبه إلى تعويم 64 بت ، وهو ما يغير قيمته
  3. يتم استدعاء COS () ، ويعود مع COS (X) في الجزء العلوي من مكدس X87 كتعويم 80bit
  4. يتم تحميل القيمة المدورة على مكدس x87 من الذاكرة
  5. القيم المستديرة وغير الجذابة لـ COS (X) تقارن غير متكافئة.

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

الإجراء cos (x) == cos (x) تم إنشاؤه في وضع الإصدار:

00DB101A  call        _CIcos (0DB1870h) 
00DB101F  fld         st(0) 
00DB1021  fucompp 

يتم حساب القيمة مرة واحدة ، ثم استنساخ ، ثم مقارنة مع نفسها - ستكون النتيجة على ما يرام

نفس الشيء في وضع التصحيح:

00A51405  sub         esp,8 
00A51408  fld         qword ptr [x] 
00A5140B  fstp        qword ptr [esp] 
00A5140E  call        @ILT+270(_cos) (0A51113h) 
00A51413  fld         qword ptr [x] 
00A51416  fstp        qword ptr [esp] 
00A51419  fstp        qword ptr [ebp-0D8h] 
00A5141F  call        @ILT+270(_cos) (0A51113h) 
00A51424  add         esp,8 
00A51427  fld         qword ptr [ebp-0D8h] 
00A5142D  fucompp          

الآن ، تحدث أشياء غريبة.
1. يتم تحميل x على FSTACK (x ، 0)
2. يتم تخزين x على المكدس العادي (اقتطاع)
3. جيب التمام يتم حسابه ، نتيجة على مكدس الطفو
4. يتم تحميل x مرة أخرى
5. يتم تخزين x على المكدس العادي (اقتطاع ، كما هو الحال الآن ، نحن "متماثلون")
6. يتم تخزين نتيجة جيب التمام الأول الذي كان على المكدس في الذاكرة ، والآن ، يحدث اقتطاع آخر للقيمة الأولى
7. يتم حساب جيب التمام ، النتيجة الثانية إذا كانت على تطفو ، ولكن تم اقتطاع هذه القيمة مرة واحدة فقط
8. يتم تحميل القيمة الأولى على FSTACK ، ولكن تم اقتطاع هذه القيمة مرتين (مرة واحدة قبل حساب جيب التمام ، مرة واحدة بعد)
9. تتم مقارنة هذه القيم 2 - نحصل على أخطاء التقريب.

يجب مطلقا لا تقارن الزوجي للمساواة في معظم الحالات. قد لا تحصل على ما تتوقعه.

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

قيم النقطة العائمة صعبة بعض الشيء. جوجل لمقارنة النقطة العائمة.

قد يكون المترجم قد أنشأ رمزًا ينتهي بمقارنة قيمة مزدوجة 64 بت مع سجل نقطة عائم داخلي 80 بت. إن اختبار قيم النقطة العائمة للمساواة عرضة لهذه الأنواع من الأخطاء - فمن الأفضل دائمًا إجراء مقارنة "غامضة" مثل (Fabs (Val1 - Val2) <epsilon) بدلاً من (Val1 == Val2).

يعد زيادة واختبار قيمة التعويم كمتغير التحكم في الحلقة فكرة سيئة حقًا. قم بإنشاء LCV منفصلة فقط للحلق ، إذا كان عليك ذلك.

في هذه الحالة يكون الأمر أسهل:

for ( int i = 90100; i<90120; i+=1 )    {
    if ( cos(i) == cos(i) )
        printf ("i==%d  OK!\n", i);
    else
        printf ("i==%d  FAIL!\n", i);
}

كيف تدور حول المشكلة؟ تعديل لو منع:

if ( (float)cos(x) == (float)cos(x) )
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top