الفرق بين تعريف المتغيرات قبل أو في الحلقة ؟
-
03-07-2019 - |
سؤال
لقد تساءلت دائما إذا كان في العامة ، معلنا رمي بعيدا متغير قبل حلقة بدلا مرارا وتكرارا داخل حلقة, يجعل أي (الأداء) الفرق ؟ 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
مقابل
2º
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 غير متوفرة بعد حلقة الجسم.ولكن لا أستطيع أن أتخيل مثل هذا الوضع اليائس ، على أية حال....
تحرير: جون السكيت جعل نقطة جيدة جدا ، مما يدل على أن المتغير الإعلان داخل حلقة يمكن أن تجعل الفعلي الدلالي الفرق.