gcc - ترجمة - votes موقع




لماذا لا يقوم GCC بتحسين*a*a*a*a*a إلى(a*a*a)*(أ*أ*أ)؟ (8)

أقوم ببعض التحسين العددي على تطبيق علمي. هناك شيء واحد لاحظته هو أن دول مجلس التعاون الخليجي ستقوم بتحسين قوة المكالمة pow(a,2) خلال تجميعها في a*a ، ولكن لم يتم تحسين استدعاء المكالمة pow(a,6) ، وسوف تستدعي بالفعل وظيفة pow في المكتبة ، والتي تتباطأ بشكل كبير الاداء. (على النقيض من ذلك ، سيزيل برنامج Intel C ++ Compiler ، الملف التنفيذي القابل للتنفيذ ، استدعاء المكتبة لـ pow(a,6) .)

ما -O3 -lm -funroll-loops -msse4 هو أنه عندما pow(a,6) محل pow(a,6) مع a*a*a*a*a*a باستخدام دول مجلس التعاون الخليجي 4.5.1 وخيارات " -O3 -lm -funroll-loops -msse4 " ، فإنه يستخدم 5 تعليمات mulsd :

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

بينما إذا كنت أكتب (a*a*a)*(a*a*a) ، سوف تنتج

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

مما يقلل من عدد التعليمات icc إلى 3. icc لديه سلوك مماثل.

لماذا لا يتعرف المجمّعون على خدعة التحسين هذه؟


حالة أخرى مشابهة: لن يقوم معظم المترجمين بتحسين a + b + c + d إلى (a + b) + (c + d) (هذا هو تحسين حيث أن التعبير الثاني يمكن أن يكون موصلاً بشكل أفضل) وتقييمه على النحو المعطى ( as (((a + b) + c) + d) ). هذا أيضا بسبب حالات الزاوية:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

هذه المخرجات 1.000000e-05 0.000000e+00


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

في الأساس العملية التالية:

pow(x,y);

لديه خطأ ملازم من نفس حجم الخطأ تقريبًا في أي ضرب أو قسمة واحدة .

في حين أن العملية التالية:

float a=someValue;
float b=a*a*a*a*a*a;

لديه خطأ متأصل أكبر من 5 أضعاف خطأ الضرب أو القسمة الواحدة (لأنك تجمع 5 مضاعفات).

يجب أن يكون المترجم متنبهًا بالفعل إلى نوع التحسين الذي يقوم به:

  1. إذا كان تحسين pow(a,6) إلى a*a*a*a*a*a فقد يؤدي ذلك إلى تحسين الأداء ، ولكنه يقلل بشكل كبير من دقة أرقام النقاط العائمة.
  2. إذا كان تحسين a*a*a*a*a*a إلى pow(a,6) فإنه قد يقلل بالفعل من الدقة لأن "a" كان بعض القيمة الخاصة التي تسمح الضرب بدون خطأ (قوة 2 أو بعض عدد صحيح صغير)
  3. إذا كان تحسين pow(a,6) إلى (a*a*a)*(a*a*a) أو (a*a)*(a*a)*(a*a) لا يزال يمكن أن يكون هناك فقدان للدقة مقارنة مع وظيفة pow .

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

الشيء الوحيد الذي يعقل (الرأي الشخصي ، وعلى ما يبدو خيار في دول مجلس التعاون الخليجي مع أي تحسين معين أو علم مترجم) لتحسين يجب أن يكون استبدال "الأسرى (أ ، 2)" مع "أ *". سيكون هذا هو الشيء الوحيد العقلاني الذي يجب على بائع المجمّعات القيام به.


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


لأن Floating Point Math ليس متلازمًا . تؤثر طريقة تجميع المعامِلات في مضاعفة النقاط العائمة على الدقة العددية للإجابة.

ونتيجة لذلك ، فإن معظم المجمعين متحفظين للغاية بشأن إعادة ترتيب حسابات النقطة العائمة ما لم يكونوا متأكدين من أن الإجابة ستبقى كما هي ، أو ما لم تخبرهم أنك لا تهتم بالدقة العددية. على سبيل المثال: الخيار -fassociative-math في gcc الذي يسمح لـ gcc بإعادة -ffast-math عمليات النقطة العائمة ، أو حتى خيار -ffast-math الذي يسمح -ffast-math أكثر دقة من السرعة ضد السرعة.


لا توجد ملصقات تشير إلى تقلص التعبيرات العائمة حتى الآن (معيار ISO C ، 6.5p8 و 7.12.2). إذا تم تعيين pragma FP_CONTRACT إلى ON ، يُسمح للمترجم أن يعتبر تعبيرًا مثل a*a*a*a*a*a كعملية واحدة ، كما لو تم تقييمه بدقة بتقطيع واحد. على سبيل المثال ، قد يقوم المترجم باستبداله بواسطة وظيفة طاقة داخلية تكون أسرع وأكثر دقة. وهذا مثير للاهتمام بشكل خاص لأن السلوك يتحكم جزئياً من قبل المبرمج مباشرة في شفرة المصدر ، بينما يمكن أحيانًا استخدام خيارات المترجم التي يوفرها المستخدم النهائي بشكل غير صحيح.

الحالة الافتراضية لـ pragma FP_CONTRACT هي معرفة بالتنفيذ ، بحيث يسمح لأحد المترجمين بإجراء مثل هذه التحسينات بشكل افتراضي. وبالتالي ، فإن الشفرة المحمولة التي تحتاج إلى اتباع قواعد IEEE 754 بصرامة يجب أن تضعها صراحةً على OFF .

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

لا تدعم دول مجلس التعاون الخليجي هذه البراغمة ، ولكن مع الخيارات الافتراضية ، فإنها تفترض أن تكون ON ؛ وبالتالي بالنسبة للأهداف باستخدام جهاز FMA ، إذا أراد أحد منع التحويل a*b+c إلى fma (a، b، c) ، يحتاج المرء إلى توفير خيار مثل -ffp-contract=off (لتعيين pragma بشكل صريح إلى OFF ) أو -std=c99 (لإعلام دول مجلس التعاون الخليجي -std=c99 مع بعض الإصدار القياسي C ، هنا C99 ، وبالتالي اتبع الفقرة أعلاه). في الماضي ، لم يكن الخيار الأخير يمنع التحول ، مما يعني أن دول مجلس التعاون الخليجي لم تكن متوافقة مع هذه النقطة: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


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

تفاجأت عندما علمت من الإجابات الأخرى أن هذا التعبير يمكن بالفعل تحسينه باستخدام مفاتيح التحويل البرمجي المناسبة. إما أن يكون التحسين أمراً تافهاً ، أو أنه حالة حافة للتحسين الأكثر شيوعاً ، أو أن كتّاب المترجمين كانوا دقيقين للغاية.

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

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


يحتوي Fortran (المصمم للحوسبة العلمية) على مشغل طاقة مدمج ، وبقدر ما أعرف أن مجمعي Fortran سيعملون بشكل عام على تحسين الارتقاء إلى القوى الصحيحة بطريقة مشابهة لما تصفه. للأسف ليس لدى C / C ++ مشغل طاقة ، فقط وظيفة المكتبة pow() . هذا لا يمنع المجمعين الأذكياء من معاملة pow بشكل خاص pow بطريقة أسرع للحالات الخاصة ، ولكن يبدو أنهم يفعلون ذلك أقل شيوعًا ...

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

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

توضيح للفضوليين: هذا لا يجد الطريقة المثلى لحساب الصلاحيات ، ولكن منذ العثور على الحل الأمثل هو مشكلة NP- كاملة وهذا ما يستحق القيام به للقوى الصغيرة على أي حال (بدلا من استخدام pow ) ، وليس هناك سبب ل ضجة مع التفاصيل.

ثم استخدمها فقط power<6>(a) .

هذا يجعل من السهل كتابة الصلاحيات (لا تحتاج إلى تهجئة 6 a s مع parens) ، -ffast-math لك هذا النوع من التحسين دون -ffast-math في حال كان لديك شيء يعتمد على الدقة مثل الجمع التعويضي (مثال على ذلك الترتيب العمليات أمر ضروري).

ربما يمكنك أيضاً نسيان أن هذا C ++ واستخدامه فقط في البرنامج C (إذا كان التحويل البرمجي مع مترجم C ++).

نأمل أن يكون هذا مفيدا.

تصحيح:

هذا ما أحصل عليه من المترجم الخاص بي:

من أجل a*a*a*a*a*a ،

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

لـ (a*a*a)*(a*a*a) ،

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

للحصول على power<6>(a) ،

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

يقوم GCC فعليًا بتحسين * a * a * a * a * a إلى (a * a * a) * (a * a * a) عندما يكون a عدد صحيح. لقد حاولت باستخدام هذا الأمر:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

هناك الكثير من أعلام مجلس التعاون الخليجي ولكن لا شيء يتوهم. وهي تعني: اقرأ من stdin؛ استخدام مستوى الأمثل O2. إدخال قائمة لغة التجميع بدلاً من ثنائي؛ يجب أن تستخدم القائمة بناء جملة لغة تجميع Intel ؛ الإدخال في لغة C (عادةً ما يتم الاستدلال على لغة من ملحق ملف الإدخال ، ولكن لا يوجد ملحق ملف عند القراءة من stdin)؛ والكتابة إلى stdout.

وهنا الجزء المهم من الانتاج. لقد أشرحته مع بعض التعليقات التي تشير إلى ما يحدث في لغة التجميع:

    ; x is in edi to begin with.  eax will be used as a temporary register.
    mov    eax, edi     ; temp1 = x
    imul    eax, edi    ; temp2 = x * temp1
    imul    eax, edi    ; temp3 = x * temp2
    imul    eax, eax    ; temp4 = temp3 * temp3

أنا أستخدم نظام دول مجلس التعاون الخليجي في Linux Mint 16 Petra ، وهو مشتق من Ubuntu. إليك إصدار مجلس التعاون الخليجي:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

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





fast-math