سؤال

هل هناك فرق في الأداء بين i++ و ++i إذا لم يتم استخدام القيمة الناتجة؟

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

المحلول

ملخص تنفيذي:لا.

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

يمكننا إظهار ذلك من خلال النظر في الكود لهذه الوظيفة ، معا مع ++i و i++.

$ cat i++.c
extern void g(int i);
void f()
{
    int i;

    for (i = 0; i < 100; i++)
        g(i);

}

الملفات هي نفسها، باستثناء ++i و i++:

$ diff i++.c ++i.c
6c6
<     for (i = 0; i < 100; i++)
---
>     for (i = 0; i < 100; ++i)

سنقوم بتجميعها، وسنحصل أيضًا على المجمّع الذي تم إنشاؤه:

$ gcc -c i++.c ++i.c
$ gcc -S i++.c ++i.c

ويمكننا أن نرى أن كلا من الكائن الذي تم إنشاؤه وملفات المجمع متماثلان.

$ md5 i++.s ++i.s
MD5 (i++.s) = 90f620dda862cd0205cd5db1f2c8c06e
MD5 (++i.s) = 90f620dda862cd0205cd5db1f2c8c06e

$ md5 *.o
MD5 (++i.o) = dd3ef1408d3a9e4287facccec53f7d22
MD5 (i++.o) = dd3ef1408d3a9e4287facccec53f7d22

نصائح أخرى

من الكفاءة مقابل النية بقلم أندرو كونيج:

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

و :

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

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

الجواب الأفضل هو ذلك ++i سيكون في بعض الأحيان أسرع ولكن ليس أبطأ أبدًا.

ويبدو أن الجميع يفترض ذلك i هو نوع مدمج عادي مثل int.وفي هذه الحالة لن يكون هناك فرق يمكن قياسه.

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

Foo Foo::operator++()
{
  Foo oldFoo = *this; // copy existing value - could be slow
  // yadda yadda, do increment
  return oldFoo;
}

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

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

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

يُفضل دائمًا إصدار البادئة على اللاحقة فيما يتعلق بالكائنات، خاصة فيما يتعلق بالمكررات.

والسبب في ذلك هو إذا نظرت إلى نمط الاتصال الخاص بالمشغلين.

// Prefix
Integer& Integer::operator++()
{
    *this += 1;
    return *this;
}

// Postfix
const Integer Integer::operator++(int)
{
    Integer oldValue = *this;
    ++(*this);
    return oldValue;
}

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

ولهذا السبب عندما ترى أمثلة تستخدم التكرارات، فإنها تستخدم دائمًا إصدار البادئة.

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

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

for (i = 0; i < 100; i++)

في كل حلقة سيكون لديك تعليمات واحدة لكل من:

  1. إضافة 1 ل i.
  2. قارن سواء i أقل من أ 100.
  3. فرع مشروط إذا i أقل من أ 100.

في حين أن حلقة التناقص:

for (i = 100; i != 0; i--)

ستحتوي الحلقة على تعليمات لكل من:

  1. إنقاص i, ، تعيين علامة حالة تسجيل وحدة المعالجة المركزية.
  2. فرع شرطي يعتمد على حالة تسجيل وحدة المعالجة المركزية (Z==0).

بالطبع هذا لا يعمل إلا عند التناقص إلى الصفر!

تم تذكره من دليل مطور نظام ARM.

اجابة قصيرة:

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

اجابة طويلة:

ما تفشل كل إجابة أخرى في ذكره هو أن الفرق بين ++i عكس i++ من المنطقي فقط في التعبير الموجود.

في حالة for(i=0; i<n; i++), ، ال i++ وحده في تعبيره الخاص:هناك نقطة تسلسل قبل i++ وهناك واحد بعد ذلك.وبالتالي فإن رمز الجهاز الوحيد الذي تم إنشاؤه هو "increase i بواسطة 1"وكيفية تسلسل ذلك بالنسبة إلى بقية البرنامج محددة جيدًا.لذلك إذا قمت بتغييره إلى البادئة ++, ، لا يهم على الإطلاق، ستظل تحصل على "زيادة" في رمز الجهاز i بواسطة 1".

الفرق بين ++i و i++ يهم فقط في تعبيرات مثل array[i++] = x; عكس array[++i] = x;.قد يجادل البعض ويقول إن الإصلاح اللاحق سيكون أبطأ في مثل هذه العمليات لأن السجل فيه i يجب إعادة تحميل الإقامة لاحقًا.لكن لاحظ بعد ذلك أن المترجم حر في طلب تعليماتك بأي طريقة يريدها، طالما أنها لا "تكسر سلوك الآلة المجردة" كما يطلق عليها معيار C.

لذلك في حين قد تفترض ذلك array[i++] = x; تتم ترجمتها إلى رمز الجهاز على النحو التالي:

  • قيمة المتجر i في السجل أ
  • تخزين عنوان المصفوفة في السجل B.
  • أضف A وB، وقم بتخزين النتائج في A.
  • في هذا العنوان الجديد الذي يمثله A، قم بتخزين قيمة x.
  • قيمة المتجر i في السجل A // غير فعال لأن التعليمات الإضافية هنا، قمنا بذلك بالفعل مرة واحدة.
  • سجل الزيادة أ.
  • سجل المتجر أ في i.

قد يقوم المترجم أيضًا بإنتاج التعليمات البرمجية بشكل أكثر كفاءة، مثل:

  • قيمة المتجر i في السجل أ
  • تخزين عنوان المصفوفة في السجل B.
  • أضف A وB، وقم بتخزين النتائج في B.
  • سجل الزيادة أ.
  • سجل المتجر أ في i.
  • ...// بقية الكود

فقط لأنك كمبرمج لغة C تم تدريبك على التفكير في أن الإصلاح اللاحق ++ في النهاية، لا يلزم ترتيب كود الآلة بهذه الطريقة.

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

"بادئة ++ دائمًا أسرع" هي في الواقع إحدى هذه المبادئ الخاطئة الشائعة بين مبرمجي لغة C المحتملين.

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

استخدم ما هو أكثر منطقية للإنسان الذي يقرأ الكود.

أولاً:الفرق بين i++ و ++i لا يكاد يذكر في C.


إلى التفاصيل.

1.مشكلة C++ المعروفة: ++i أسرع

في لغة سي++، ++i هو أكثر كفاءة إذا i هو نوع من الكائنات مع عامل زيادة مثقل.

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

وكما ذكر أعلاه، فإن هذا لا ينطبق على الأنواع الأساسية.

2.الحقيقة التي لا يعرفها إلا القليل: i++ يمكن يكون أسرع

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

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

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

لقد اختبرت ذلك للتو مع المترجمين الذين نستخدمهم في مشروعنا الحالي ولم يقوم 3 من أصل 4 بتحسينه.

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

إذا لم يكن لديك تطبيق غبي حقًا لأحد العوامل في التعليمات البرمجية الخاصة بك:

كنت أفضّل دائمًا ++i على i++.

في لغة C، يمكن للمترجم بشكل عام تحسينها لتكون هي نفسها إذا كانت النتيجة غير مستخدمة.

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

يمكنني التفكير في موقف يكون فيه postfix أبطأ من زيادة البادئة:

تخيل المعالج مع التسجيل A يتم استخدامه كمراكم وهو السجل الوحيد المستخدم في العديد من التعليمات (بعض وحدات التحكم الدقيقة الصغيرة هي في الواقع مثل هذا).

الآن تخيل البرنامج التالي وترجمته إلى تجميع افتراضي:

زيادة البادئة:

a = ++b + c;

; increment b
LD    A, [&b]
INC   A
ST    A, [&b]

; add with c
ADD   A, [&c]

; store in a
ST    A, [&a]

زيادة ما بعد الإصلاح:

a = b++ + c;

; load b
LD    A, [&b]

; add with c
ADD   A, [&c]

; store in a
ST    A, [&a]

; increment b
LD    A, [&b]
INC   A
ST    A, [&b]

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

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


كملاحظة جانبية، أود أن أذكر أن التعبير الذي يوجد فيه b++ لا يمكن ببساطة تحويلها إلى واحدة مع ++b دون أي جهد إضافي (على سبيل المثال عن طريق إضافة ملف - 1).فالمقارنة بين الاثنين إذا كانا جزءًا من بعض العبارات غير صحيحة حقًا.في كثير من الأحيان، حيث تستخدم b++ داخل تعبير لا يمكنك استخدامه ++b, ، حتى لو ++b كان من المحتمل أن تكون أكثر كفاءة، فإنه سيكون ببساطة خطأ.الاستثناء يكون بالطبع إذا كان التعبير يتوسل إليه (على سبيل المثال a = b++ + 1; والتي يمكن تغييرها إلى a = ++b;).

أنا أفضّل دائمًا الزيادة المسبقة، ولكن ...

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

لذلك، لأغراض عملية، من المحتمل ألا يكون هناك فرق كبير بين أداء النموذجين.ومع ذلك، أفضّل دائمًا الزيادة المسبقة لأنه يبدو من الأفضل التعبير بشكل مباشر عما أحاول قوله، بدلاً من الاعتماد على المُحسِّن لمعرفة ذلك.

كما أن إعطاء المحسن أقل ما يمكن القيام به يعني أن المترجم يعمل بشكل أسرع.

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

myArray[i++] = "hello";

ضد

myArray[++i] = "hello";

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

جمعية البناء الخيرية.

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