سؤال

لقد تساءلت دائما إذا كان في العامة ، معلنا رمي بعيدا متغير قبل حلقة بدلا مرارا وتكرارا داخل حلقة, يجعل أي (الأداء) الفرق ؟ A (من غير المجدي جدا) على سبيل المثال في جاوة:

أ) الإعلان قبل الحلقة:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

ب) إعلان (مرارا وتكرارا) داخل الحلقة:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

واحد الذي هو أفضل ، a أو ب?

وأظن أن تكرار متغير الإعلان (على سبيل المثال ب) يخلق المزيد من النفقات العامة في نظرية, لكن هذا المجمعين هي ذكية بما فيه الكفاية بحيث لا يهم.على سبيل المثال ب لديها ميزة كونها أكثر إحكاما و الحد من نطاق المتغير حيث يتم استخدامه.لا يزال, أنا أميل إلى رمز حسب المثال a.

تحرير: أنا مهتم بشكل خاص في جافا القضية.

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

المحلول

الذي هو أفضل ، a أو ب?

من منظور الأداء سيكون لديك لقياس ذلك.(و في رأيي, إذا كنت يمكن قياس الفرق, مترجم ليست جيدة جدا).

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

نصائح أخرى

حسنا ركضت الخاص بك A و B أمثلة 20 مرة كل حلقات 100 مليون مرة.(JVM - 1.5.0)

A:متوسط وقت التنفيذ:.074 ثانية

ب:متوسط وقت التنفيذ :.067 ثانية

لدهشتي ب كان أسرع قليلا.بأسرع أجهزة الكمبيوتر الآن من الصعب القول ما إذا كنت يمكن أن تقيس بدقة هذا.أود أن الرمز هو الطريقة بشكل جيد ولكن أود أن أقول أنه لا يهم حقا.

ذلك يعتمد على اللغة و استخدامها بالضبط.فعلى سبيل المثال ، في C# 1 أنه لا فرق.في C# 2 إذا كان المتغير المحلي يتم القبض عليه من قبل مجهول الأسلوب (أو امدا التعبير في C# 3) يمكن أن تجعل جدا signficant الفرق.

على سبيل المثال:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

الإخراج:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

الفرق هو أن جميع الإجراءات التقاط نفس outer متغير, ولكن لكل منها منفصلة inner متغير.

وفيما يلي ما كتبته وجمعت في .صافي.

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

هذا هو ما يحصل من .صافي عاكس عندما CIL هو المقدمة مرة أخرى إلى رمز.

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

لذا على حد سواء تبدو بالضبط نفس بعد التجميع.في إدارتها اللغات البرمجية يتم تحويلها إلى CL/رمز بايت و في وقت التنفيذ يتم تحويلها إلى لغة الآلة.حتى في لغة الآلة مزدوجة قد لا يكون تم إنشاؤها على المكدس.قد يكون مجرد تسجيل رمز تعكس أنه هو متغير مؤقت لمدة WriteLine وظيفة.هناك مجموعة كاملة الأمثل القواعد فقط عن الحلقات.حتى الرجل المتوسط لا ينبغي أن تشعر بالقلق حيال ذلك ، خاصة في إدارتها اللغات.هناك حالات عندما يمكنك تحسين إدارة المدونة ، على سبيل المثال ، إذا كان لديك لسلسلة عدد كبير من السلاسل باستخدام فقط string a; a+=anotherstring[i] مقابل استخدام StringBuilder.هناك اختلاف كبير جدا في الأداء بين البلدين.هناك الكثير من مثل هذه الحالات حيث المترجم لا يمكن تحسين التعليمات البرمجية الخاصة بك, لأنه لا يمكن معرفة ما هو المقصود في أكبر نطاق.ولكن فإنه يمكن إلى حد كبير تحسين الأشياء الأساسية بالنسبة لك.

هذا هو مسكتك في VB.NET.Visual Basic النتيجة لن تهيئة المتغير في هذا المثال:

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

وهذا طباعة 0 المرة الأولى (Visual Basic المتغيرات القيم الافتراضية عندما أعلن!) ولكن i في كل مرة بعد ذلك.

إذا قمت بإضافة = 0, ، على الرغم من أنك تحصل على ما قد تتوقع:

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...

أنا قدمت اختبار بسيط:

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

مقابل

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

لقد جمعت هذه الرموز مع دول مجلس التعاون الخليجي - 5.2.0.ثم تفكيكها الرئيسية () هذه اثنين من رموز و هذه هي النتيجة:

1º:

   0x00000000004004b6 <+0>:     push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

مقابل

   0x00000000004004b6 <+0>: push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

والتي هي exaclty نفس asm النتيجة.ليس دليلا على أن اثنين من رموز تنتج نفس الشيء ؟

أنا دائما استخدام (بدلا من الاعتماد على مترجم) و يمكن أيضا كتابة إلى:

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

هذا لا يزال يقيد intermediateResult إلى حلقة نطاق, ولكن لا redeclare خلال كل التكرار.

وهي لغة تعتمد - IIRC C# يحسن هذا, لذلك ليس هناك أي فرق لكن جافا سكريبت (على سبيل المثال) سوف نفعل كل تخصيص الذاكرة كوخ في كل مرة.

في رأيي, ب هو أفضل هيكل.في آخر قيمة intermediateResult العصي بعد حلقة انتهى.

تحرير:هذا لا يجعل الكثير من الفرق مع أنواع قيمة ، ولكن أنواع مرجع يمكن أن يكون نوعا من الثقل.شخصيا, أنا أحب المتغيرات dereferenced في أقرب وقت ممكن من أجل تنظيف ، ب يفعل ذلك بالنسبة لك ،

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

كقاعدة عامة, أعلن بلدي المتغيرات في الداخلية نطاق ممكن.لذا ، إذا كنت لا تستخدم intermediateResult خارج الحلقة ، ثم كنت أذهب مع B.

زميل يفضل النموذج الأول ، تقول هو الأمثل ، مفضلا إعادة استخدام الإعلان.

أنا أفضل الثانية (و في محاولة لإقناع لي زميل في العمل!;- )) بعد قراءة هذا:

  • فإنه يقلل من نطاق المتغيرات إلى حيث تكون هناك حاجة إليها, وهو أمر جيد.
  • جافا يحسن يكفي لجعل لا يوجد فرق كبير في الأداء.IIRC, ولعل النموذج الثاني هو حتى أسرع.

على أي حال, انها تقع في فئة المبكرة الأمثل التي تعتمد في نوعية مترجم و/أو JVM.

هناك فرق في C# إذا كنت تستخدم متغير في امدا, الخ.ولكن في عام المترجم أساسا أن تفعل الشيء نفسه ، على افتراض متغير يستخدم فقط داخل الحلقة.

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

ومن ثم نسخة يزعجني إلى ما لا نهاية ، لأنه ليس هناك فائدة من الأمر ويجعل الأمر أكثر صعوبة بكثير بسبب حول رمز...

حسنا, يمكنك دائما جعل نطاق هذا:

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

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

لقد اعتقدت دائما أنه إذا كنت تقوم بتعريف المتغيرات داخل حلقة الخاص بك ثم أنت تضيع الذاكرة.إذا كان لديك شيء من هذا القبيل:

for(;;) {
  Object o = new Object();
}

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

ومع ذلك ، إذا كان لديك هذا:

Object o;
for(;;) {
  o = new Object();
}

ثم أنت فقط إيجاد مرجعية واحدة وتعيين وجوه جديدة لها في كل مرة.ومن المؤكد أنه قد يستغرق وقتا أطول قليلا من أجل أن تخرج من نطاق, ولكن هناك واحد فقط التعلق الإشارة إلى التعامل مع.

أعتقد أن ذلك يعتمد على مترجم و من الصعب إعطاء إجابة عامة.

بلدي الممارسة التالية:

  • إذا كان نوع المتغير بسيط (int, double, ...) أنا أفضل الخيار ب (في الداخل).
    السبب: الحد من نطاق متغير.

  • إذا كان نوع المتغير ليست بسيطة (نوع من class أو struct) أنا أفضل الخيار a (خارج).
    السبب: والحد من عدد من المنشئ-dtor المكالمات.

من منظور الأداء ، الخارج (الكثير) أفضل.

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

لقد نفذت كل الوظائف 1 مليار مرات لكل منهما.خارج() أخذت 65 ميلي ثانية.داخل() أخذت 1.5 ثانية.

أ) هو رهان آمن من ب).........تخيل لو كنت تهيئة هيكل في حلقة بدلا من 'int' أو 'تعويم' ثم ماذا ؟

مثل

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

أنت بالتأكيد لا بد أن تواجه مشاكل مع تسرب الذاكرة!.وبالتالي أعتقد 'A' هو الرهان الأكثر أمانا في حين أن 'ب' هو عرضة الذاكرة تراكم esp العمل بالقرب من مصدر المكتبات.يمكنك التحقق من usinng 'Valgrind' أداة على لينكس تحديدا الأداة الفرعية 'Helgrind'.

انها مسألة مثيرة للاهتمام.من تجربتي هناك في نهاية المطاف المسألة في الاعتبار عند مناقشة هذا الموضوع على المدونة:

هل هناك أي سبب لماذا المتغير سوف تحتاج إلى أن تكون عالمية ؟

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

أيضا ، كملاحظة جانبية, لا تكرار المتغير المحلي الأسماء بين أساليب مختلفة حتى لو أغراضها هي شبه متطابقة;فإنه يحصل فقط مربكة.

أنا اختبار JS مع عقدة 4.0.0 إذا كان أي شخص مهتم.معلنا خارج حلقة أدى إلى ~.5 ms تحسين الأداء في المتوسط أكثر من 1000 التجارب مع 100 مليون حلقة التكرار في المحاكمة.لذا سأقول استمر في الكتابة في أكثر قابلية للقراءة / للصيانة الطريقة B, المنظمة البحرية الدولية.أود أن أضع كود في كمان لكن انا استخدمت الأداء الآن وحدة عقدة.هنا كود:

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)

هذا هو أفضل شكل

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1) في هذا الطريق المعلنة مرة كل متغير وليس كل دورة.2) إحالة الأمر فاتسير ثيان كل خيار آخر.3) حتى bestpractice القاعدة أي إعلان خارج عن التكرار.

حاولت نفس الشيء في الذهاب ، مقارنة مترجم الإخراج باستخدام go tool compile -S مع 1.9.4

فرق صفر كما في المجمع الانتاج.

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

الخلاصة: بالنسبة مثل هذه الحالات هناك لا الفارق في الأداء.

خارج حلقة الحالة

int intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i+2;
    System.out.println(intermediateResult);
}

داخل حلقة الحالة

for(int i=0; i < 1000; i++){
    int intermediateResult = i+2;
    System.out.println(intermediateResult);
}

راجعت الملف تم تجميعها على IntelliJ هو decompiler و لكل من الحالتين حصلت على نفس Test.class

for(int i = 0; i < 1000; ++i) {
    int intermediateResult = i + 2;
    System.out.println(intermediateResult);
}

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

خارج حلقة الحالة

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

داخل حلقة الحالة

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

إذا كنت إيلاء اهتمام وثيق ، Slot تعيين i و intermediateResult في LocalVariableTable هو تبديل المنتج من أجل الظهور.نفس الفرق في فتحة ينعكس في الأسطر من التعليمات البرمجية.

  • أي عملية إضافية يتم تنفيذها
  • intermediateResult لا يزال متغير محلي في كلتا الحالتين لذا لا يوجد فرق وقت الوصول.

مكافأة

المجمعين تفعل الكثير من التحسين ، نلقي نظرة على ما يحدث في هذه الحالة.

العمل الصفر الحالة

for(int i=0; i < 1000; i++){
    int intermediateResult = i;
    System.out.println(intermediateResult);
}

العمل الصفر decompiled

for(int i = 0; i < 1000; ++i) {
    System.out.println(i);
}

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

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

تحرير: جون السكيت جعل نقطة جيدة جدا ، مما يدل على أن المتغير الإعلان داخل حلقة يمكن أن تجعل الفعلي الدلالي الفرق.

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