سؤال

لدينا السؤال هل هناك فرق في الأداء بين i++ و ++i شركة?

ما هو الجواب على C++؟

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

المحلول

[ملخص تنفيذي:يستخدم ++i إذا لم يكن لديك سبب محدد للاستخدام i++.]

بالنسبة لـ C++، الإجابة أكثر تعقيدًا بعض الشيء.

لو i هو نوع بسيط (ليس مثيلًا لفئة C++)، ثم الإجابة المقدمة لـ C ("لا يوجد فرق في الأداء") يحمل، لأن المترجم يقوم بإنشاء التعليمات البرمجية.

ومع ذلك، إذا i هو مثيل لفئة C++، إذن i++ و ++i يقومون بإجراء مكالمات إلى أحد operator++ المهام.فيما يلي زوج قياسي من هذه الوظائف:

Foo& Foo::operator++()   // called for ++i
{
    this->data += 1;
    return *this;
}

Foo Foo::operator++(int ignored_dummy_value)   // called for i++
{
    Foo tmp(*this);   // variable "tmp" cannot be optimized away by the compiler
    ++(*this);
    return tmp;
}

نظرًا لأن المترجم لا يقوم بإنشاء تعليمات برمجية، بل يقوم فقط باستدعاء operator++ الوظيفة، لا توجد وسيلة لتحسين بعيدا tmp المتغير ومنشئ النسخ المرتبط به.إذا كان مُنشئ النسخ باهظ الثمن، فقد يكون لذلك تأثير كبير على الأداء.

نصائح أخرى

نعم.هنالك.

قد يتم أو لا يتم تعريف عامل التشغيل ++ كدالة.بالنسبة للأنواع البدائية (int، double، ...) تكون عوامل التشغيل مدمجة، لذلك من المحتمل أن يكون المترجم قادرًا على تحسين التعليمات البرمجية الخاصة بك.ولكن في حالة الكائن الذي يحدد عامل التشغيل ++، فإن الأمور مختلفة.

يجب أن تقوم وظيفة عامل التشغيل ++(int) بإنشاء نسخة.وذلك لأنه من المتوقع أن يُرجع postfix ++ قيمة مختلفة عما يحمله:يجب أن يحتفظ بقيمته في متغير مؤقت، ويزيد قيمته ويعيد درجة الحرارة.في حالة عامل التشغيل ++()، البادئة ++، ليست هناك حاجة لإنشاء نسخة:يمكن للكائن أن يزيد من نفسه ثم يعود بنفسه ببساطة.

وهنا توضيح لهذه النقطة:

struct C
{
    C& operator++();      // prefix
    C  operator++(int);   // postfix

private:

    int i_;
};

C& C::operator++()
{
    ++i_;
    return *this;   // self, no copy created
}

C C::operator++(int ignored_dummy_value)
{
    C t(*this);
    ++(*this);
    return t;   // return a copy
}

في كل مرة تقوم فيها باستدعاء عامل التشغيل ++(int) يجب عليك إنشاء نسخة، ولا يستطيع المترجم فعل أي شيء حيال ذلك.عندما يُتاح لك الاختيار، استخدم عامل التشغيل++();بهذه الطريقة لا تحفظ نسخة.قد يكون مهمًا في حالة الزيادات العديدة (حلقة كبيرة؟) و/أو الكائنات الكبيرة.

فيما يلي معيار للحالة عندما تكون عوامل الزيادة في وحدات ترجمة مختلفة.مترجم مع g++ 4.5.

تجاهل مشاكل النمط في الوقت الحالي

// a.cc
#include <ctime>
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};

int main () {
    Something s;

    for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
    std::clock_t a = clock();
    for (int i=0; i<1024*1024*30; ++i) ++s;
    a = clock() - a;

    for (int i=0; i<1024*1024*30; ++i) s++; // warm up
    std::clock_t b = clock();
    for (int i=0; i<1024*1024*30; ++i) s++;
    b = clock() - b;

    std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
              << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
    return 0;
}

يا (ن) الزيادة

امتحان

// b.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    for (auto it=data.begin(), end=data.end(); it!=end; ++it)
        ++*it;
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

نتائج

النتائج (التوقيت بالثواني) مع g++ 4.5 على جهاز افتراضي:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

يا (1) زيادة

امتحان

لنأخذ الآن الملف التالي:

// c.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

لا يفعل شيئا في الزيادة.هذا يحاكي الحالة عندما تكون الزيادة ذات تعقيد مستمر.

نتائج

تختلف النتائج الآن بشكل كبير:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

خاتمة

أداء الحكمة

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

الدلالي الحكيم

  • i++ يقول increment i, I am interested in the previous value, though.
  • ++i يقول increment i, I am interested in the current value أو increment i, no interest in the previous value.مرة أخرى، سوف تعتاد على ذلك، حتى لو لم تكن كذلك الآن.

نوث.

التحسين المبكر هو أصل كل الشرور.كما هو التشاؤم السابق لأوانه.

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

في المثال التالي، الكود الذي تم إنشاؤه مطابق للبادئة واللاحقة، على سبيل المثال:

#include <stdio.h>

class Foo
{
public:

    Foo() { myData=0; }
    Foo(const Foo &rhs) { myData=rhs.myData; }

    const Foo& operator++()
    {
        this->myData++;
        return *this;
    }

    const Foo operator++(int)
    {
        Foo tmp(*this);
        this->myData++;
        return tmp;
    }

    int GetData() { return myData; }

private:

    int myData;
};

int main(int argc, char* argv[])
{
    Foo testFoo;

    int count;
    printf("Enter loop count: ");
    scanf("%d", &count);

    for(int i=0; i<count; i++)
    {
        testFoo++;
    }

    printf("Value: %d\n", testFoo.GetData());
}

سواء استخدمت ++testFoo أو testFoo++، فستظل تحصل على نفس الكود الناتج.في الواقع، بدون قراءة العدد الوارد من المستخدم، تمكن المُحسِّن من تحويل الأمر برمته إلى رقم ثابت.إذا هذا:

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

نتج عنها ما يلي:

00401000  push        0Ah  
00401002  push        offset string "Value: %d\n" (402104h) 
00401007  call        dword ptr [__imp__printf (4020A0h)] 

لذلك، في حين أنه من المؤكد أن إصدار postfix قد يكون أبطأ، فمن المحتمل أن يكون المحسن جيدًا بما يكفي للتخلص من النسخة المؤقتة إذا كنت لا تستخدمه.

ال دليل أسلوب جوجل C++ يقول:

الزيادة المسبقة والنقصان المسبق

استخدم نموذج البادئة (++ i) لمشغلي الزيادة والانخفاض مع التكرار وغيرها من كائنات القالب.

تعريف: عندما يتم زيادة المتغير (++ i أو i ++) أو الانخفاض (-i أو i--) ولا يتم استخدام قيمة التعبير ، يجب على المرء أن يقرر ما إذا كان يجب أن يكون مسبقًا (انخفاض) أو postincrement (الانخفاض).

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

سلبيات: تطور التقليد ، في C ، لاستخدام ما بعد الزيادة عندما لا يتم استخدام قيمة التعبير ، خاصة في الحلقات.يجد البعض بعد الزيادة أسهل في القراءة ، لأن "الموضوع" (i) يسبق "فعل" ( ) ، تماما كما هو الحال في اللغة الإنجليزية.

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

أود أن أشير إلى مشاركة ممتازة كتبها Andrew Koenig في Code Talk مؤخرًا.

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

في شركتنا أيضًا نستخدم اتفاقية ++iter لتحقيق الاتساق والأداء حيثما أمكن ذلك.لكن أندرو يثير تفاصيل تم التغاضي عنها فيما يتعلق بالقصد مقابل الأداء.هناك أوقات نريد فيها استخدام iter++ بدلاً من ++iter.

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

@كيتان

...يثير تفاصيل تم الإفراط في النظر إليها فيما يتعلق بالقصد مقابل الأداء.هناك أوقات نريد فيها استخدام iter++ بدلاً من ++iter.

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

بعد كل هذا هو السبب وراء عدم تسمية اللغة "++C".[*]

[*] أدخل مناقشة إلزامية حول ++C كونه اسم أكثر منطقية.

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

ومع ذلك، لا يزال الأسلوب الأفضل هو استخدام ++iter في معظم الحالات.:-)

فرق الأداء بين ++i و i++ سيكون أكثر وضوحًا عندما تفكر في العوامل كوظائف إرجاع القيمة وكيفية تنفيذها.لتسهيل فهم ما يحدث، سيتم استخدام أمثلة التعليمات البرمجية التالية int كما لو كان أ struct.

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

int& int::operator++() { 
     return *this += 1;
}

لكن الشيء نفسه لا يمكن أن يقال عنه i++.

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

int int::operator++(int& _Val) {
    int _Original = _Val;
    _Val += 1;
    return _Original;
}

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

  1. ++أنا - أسرع عدم استخدام قيمة الإرجاع
  2. أنا++ - أسرع استخدام قيمة الإرجاع

متى عدم استخدام القيمة المرجعة التي يضمن المترجم عدم استخدامها مؤقتًا في حالة ++أنا.ليس مضمونًا أن تكون أسرع، ولكن مضمونًا ألا تكون أبطأ.

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

أحد الأسباب التي تدفعك إلى استخدام ++i حتى في الأنواع المضمنة حيث لا توجد ميزة في الأداء هو خلق عادة جيدة لنفسك.

@علامة:لقد حذفت إجابتي السابقة لأنها كانت مقلوبًا بعض الشيء، واستحقت التصويت السلبي لذلك وحده.أعتقد في الواقع أنه سؤال جيد لأنه يسأل عما يدور في أذهان الكثير من الناس.

الإجابة المعتادة هي أن ++i أسرع من i++، ولا شك أنه كذلك، ولكن السؤال الأكبر هو "متى يجب أن تهتم؟"

إذا كان جزء وقت وحدة المعالجة المركزية المستغرق في زيادة التكرارات أقل من 10%، فقد لا تهتم.

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

لقد رأيت مثالاً حيث كانت زيادة المكرر تستهلك ما يزيد عن 90٪ من الوقت.في هذه الحالة، سيؤدي الانتقال إلى زيادة الأعداد الصحيحة إلى تقليل وقت التنفيذ بمقدار هذا المقدار بشكل أساسي.(أي.أفضل من تسريع 10x)

كان السؤال المقصود هو متى تكون النتيجة غير مستخدمة (وهذا واضح من السؤال الخاص بـ C).هل يمكن لأي شخص إصلاح هذا لأن السؤال هو "مجتمع ويكي"؟

غالبًا ما يتم الاستشهاد بـ Knuth حول التحسينات المبكرة.صحيح.لكن دونالد كنوث لن يدافع أبدًا بهذا الكود الرهيب الذي يمكنك رؤيته في هذه الأيام.هل سبق لك أن رأيت a = b + c بين الأعداد الصحيحة في Java (وليس int)؟وهذا يصل إلى 3 تحويلات للملاكمة/فتح الملاكمة.تجنب أشياء من هذا القبيل هو المهم.وكتابة i++ بدلاً من ++i بلا فائدة هو نفس الخطأ.يحرر:وكما قال فريسنل بشكل لطيف في أحد التعليقات، يمكن تلخيص ذلك على النحو التالي: "التحسين المبكر أمر شرير، كما هو الحال مع التشاؤم المبكر".

حتى حقيقة أن الناس أكثر اعتيادًا على i++ هي إرث C مؤسف، ناجم عن خطأ مفاهيمي من K&R (إذا اتبعت حجة النية، فهذا استنتاج منطقي؛والدفاع عن K&R لأنهم K&R لا معنى له، إنهم رائعون، لكنهم ليسوا رائعين كمصممي لغة؛توجد أخطاء لا تعد ولا تحصى في تصميم لغة C، بدءًا من gets() إلى strcpy()، إلى strncpy() API (كان من المفترض أن تحتوي على strlcpy() API منذ اليوم الأول)).

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

@wilhelmtell

يمكن للمترجم حذف المؤقت.حرفيا من الموضوع الآخر:

يُسمح لمترجم C++ بإزالة المؤقتات المستندة إلى المكدس حتى لو أدى ذلك إلى تغيير سلوك البرنامج.رابط MSDN لـ VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

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

في الأساس، الحيلة هي استخدام فئة مساعدة لتأجيل الزيادة بعد العودة، ويأتي RAII للإنقاذ

#include <iostream>

class Data {
    private: class DataIncrementer {
        private: Data& _dref;

        public: DataIncrementer(Data& d) : _dref(d) {}

        public: ~DataIncrementer() {
            ++_dref;
        }
    };

    private: int _data;

    public: Data() : _data{0} {}

    public: Data(int d) : _data{d} {}

    public: Data(const Data& d) : _data{ d._data } {}

    public: Data& operator=(const Data& d) {
        _data = d._data;
        return *this;
    }

    public: ~Data() {}

    public: Data& operator++() { // prefix
        ++_data;
        return *this;
    }

    public: Data operator++(int) { // postfix
        DataIncrementer t(*this);
        return *this;
    }

    public: operator int() {
        return _data;
    }
};

int
main() {
    Data d(1);

    std::cout <<   d << '\n';
    std::cout << ++d << '\n';
    std::cout <<   d++ << '\n';
    std::cout << d << '\n';

    return 0;
}

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

كلاهما سريع ;) إذا كنت تريد أن يكون نفس الحساب للمعالج ، فهو مجرد ترتيب يتم به الاختلاف.

على سبيل المثال الكود التالي :

#include <stdio.h>

int main()
{
    int a = 0;
    a++;
    int b = 0;
    ++b;
    return 0;
}

إنتاج التجمع التالي:

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    $0x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

ترى أنه بالنسبة لـ a++ وb++، فهي عبارة عن وسيلة تذكير متضمنة، لذا فهي نفس العملية؛)

عندما تكتب i++ أنت تطلب من المترجم أن يزيد بعد أن ينتهي من هذا السطر أو الحلقة.

++i يختلف قليلا عن i++.في i++ يمكنك زيادة بعد الانتهاء من الحلقة ولكن ++i يمكنك الزيادة مباشرة قبل انتهاء الحلقة.

++i أسرع من i++ لأنه لا يُرجع نسخة قديمة من القيمة.

كما أنها أكثر سهولة:

x = i++;  // x contains the old value of i
y = ++i;  // y contains the new value of i 

هذا المثال C طباعة "02" بدلاً من "12" الذي قد تتوقعه:

#include <stdio.h>

int main(){
    int a = 0;
    printf("%d", a++);
    printf("%d", ++a);
    return 0;
}

نفس الشيء بالنسبة لـ C ++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top