2015-06-27 9 views
5

Ich bin es gewohnt, dies zu tun:Ist es besser, eine lokale innerhalb oder außerhalb einer Schleife zu deklarieren?

do 
    local a 
    for i=1,1000000 do 
     a = <some expression> 
     <...> --do something with a 
    end 
end 

statt

for i=1,1000000 do 
    local a = <some expression> 
    <...> --do something with a 
end 

Meine Argumentation ist, dass eine lokale Variable 1000000 mal schaffen, ist weniger effizient als es nur einmal erstellen und wiederverwenden es bei jeder Iteration.

Meine Frage ist: Ist das wahr oder gibt es ein anderes technisches Detail, das ich vermisse? Ich frage, weil ich niemanden sehe, der das macht, aber nicht sicher ist, ob der Grund dafür ist, dass der Vorteil zu klein ist oder weil es in der Tat schlimmer ist. Umso besser, ich benutze weniger Speicher und laufe schneller.

+0

"Erstellen einer lokalen Variablen" ist etwas, was Sie tun, wenn Sie Quellcode schreiben. Was zur Laufzeit passiert, ist, wie ich annehme, Ihnen so unbekannt wie mir. (Okay, ich habe mit 'Luac -l' gespielt und etwas über den VM-Befehlssatz gelesen.) –

Antwort

9

Wie bei jeder Performance-Frage zuerst messen. In einem Unix-System können Sie Zeit verwenden:

time lua -e 'local a; for i=1,100000000 do a = i * 3 end' 
time lua -e 'for i=1,100000000 do local a = i * 3 end' 

die Ausgabe:

real 0m2.320s 
user 0m2.315s 
sys 0m0.004s 

real 0m2.247s 
user 0m2.246s 
sys 0m0.000s 

Je mehr lokale Version erscheint einen kleinen Prozentsatz schneller in Lua zu sein, da es nicht a auf Null nicht initialisiert werden. Doch dass es kein Grund, es zu benutzen ist, den meisten lokalen Bereich verwenden, weil sie es besser lesbar ist (das ist guter Stil in allen Sprachen: Um diese Frage sieht für C fragte Java und C#)

Wenn Wenn Sie eine Tabelle erneut verwenden, anstatt sie in der Schleife zu erstellen, ist der Leistungsunterschied wahrscheinlich größer. In jedem Fall sollten Sie die Lesbarkeit messen und bevorzugen, wann immer Sie können.

+5

Spot auf. Lesbarkeit ist 100-mal wichtiger als _unwesentliche_ Leistungsoptimierungen. –

+1

COLD RUN! Machen Sie ein paar Male "erste & zweite" in der Schale und Sie werden sehen, dass es kein strenges - schnelleres gibt. ** 1253 **/1022, 1020/1022, 1023/1019, 1020/1021, 1022/1020, 1022/1022, 1028/1019, 1021/1021, 1021/1022, ... – user3125367

+0

Eigentlich gibt es beides Fälle zwei vordefinierte Slots in Aktivierungsaufzeichnung und es gibt keinen Unterschied in Opcodes. – user3125367

2

Bitte beachten Sie: Die Definition der Variablen innerhalb der Schleife stellt sicher, dass nach einer Iteration dieser Schleife die nächste Iteration diese gespeicherte Variable nicht erneut verwenden kann. Wenn Sie es vor der for-Schleife definieren, ist es möglich, eine Variable durch mehrere Iterationen zu übertragen, wie jede andere Variable, die nicht innerhalb der Schleife definiert ist.

Weiter, um Ihre Frage zu beantworten: Ja, es ist weniger effizient, weil es die Variable neu initiiert. Wenn der Lua JIT-/Compiler eine gute Mustererkennung hat, kann es sein, dass er nur die Variable zurücksetzt, aber das kann ich weder bestätigen noch dementieren.

+0

Sorry, ich bin verwirrt. Auf welches beziehen Sie sich, wenn Sie sagen "Ja, es ist weniger effizient"? In beiden Variablen wird jede Iteration neu definiert. – Mandrill

+0

Sorry für die Verwirrung. Ich erwähnte die In-Loop-Deklaration von Variablen beim Schreiben von "Ja, es ist weniger effizient". – vdMeent

5

Ich denke, es gibt einige Verwirrung über die Art, wie Compiler mit Variablen umgehen. Aus einer hochrangigen Art menschlicher Sichtweise ist es natürlich, daran zu denken, eine Variable zu definieren und zu zerstören, um damit irgendeine Art von "Kosten" zu verbinden.

Das ist jedoch nicht unbedingt der optimierende Compiler. Die Variablen, die Sie in einer höheren Sprache erstellen, ähneln eher temporären "Handles" im Speicher. Der Compiler betrachtet diese Variablen und übersetzt sie dann in eine Zwischendarstellung (etwas, das näher an der Maschine liegt) und ermittelt, wo alles gespeichert werden soll, hauptsächlich mit dem Ziel der Zuweisung von Registern (die unmittelbarste Form von Speicher für die CPU). Dann übersetzt es das IR in Maschinencode, wo die Idee einer "Variablen" gar nicht existiert, nur Orte zum Speichern von Daten (Register, Cache, Dram, Disk).

Dieser Prozess beinhaltet die Wiederverwendung derselben Register für mehrere Variablen, vorausgesetzt, dass sie sich nicht gegenseitig stören (vorausgesetzt, sie werden nicht gleichzeitig benötigt: nicht gleichzeitig "live").

Anders ausgedrückt, mit Code wie:

local a = <some expression> 

Die resultierende Anordnung so etwas wie sein könnte:

load gp_register, <result from expression> 

... oder es kann bereits von einem Ausdruck in einem Register das Ergebnis , und die Variable verschwindet schließlich vollständig (nur mit dem gleichen Register dafür).

... was bedeutet, dass es keine "Kosten" für die Existenz der Variablen gibt. Es übersetzt sich einfach direkt in ein Register, das immer verfügbar ist. Es gibt keine "Kosten" für "Erstellen eines Registers", da Register immer vorhanden sind.

Wenn Sie beginnen, Variablen in einem breiteren (weniger lokalen) Bereich zu erstellen, im Gegensatz zu dem, was Sie denken, können Sie verlangsamen den Code. Wenn Sie dies oberflächlich tun, kämpfen Sie gegen die Registerzuordnung des Compilers und machen es dem Compiler schwerer, herauszufinden, welche Register für was zuzuteilen sind. In diesem Fall könnte der Compiler mehr Variablen in den Stapel einbringen, was weniger effizient ist und tatsächlich Kosten verursacht. Ein intelligenter Compiler kann immer noch gleich effizienten Code ausgeben, aber Sie könnten die Dinge tatsächlich langsamer machen . Dem Compiler hier zu helfen, bedeutet oft mehr lokale Variablen, die in kleineren Bereichen verwendet werden, in denen Sie die besten Chancen auf Effizienz haben.

Im Assembler-Code ist die Wiederverwendung der gleichen Register wann immer möglich, effizient, um das Verschütten von Stapeln zu vermeiden. In Hochsprachen mit Variablen ist das Gegenteil der Fall. Reduzieren Sie den Umfang der Variablen hilft der Compiler herauszufinden, welche Register es wiederverwenden kann, da die Verwendung eines lokalen Bereichs für Variablen hilft, den Compiler zu informieren, welche Variablen nicht gleichzeitig aktiv sind.

Jetzt gibt es Ausnahmen, wenn Sie beginnen, benutzerdefinierte Konstruktor- und Destruktorlogik in Sprachen wie C++ zu verwenden, wo die Wiederverwendung eines Objekts redundante Konstruktion und die Zerstörung eines Objekts verhindern kann, das wiederverwendet werden kann. Aber das gilt nicht in einer Sprache wie Lua, wo alle Variablen im Grunde einfach alte Daten sind (oder in von Müll gesammelten Daten oder Benutzerdaten verarbeitet werden).

Der einzige Fall, in dem Sie möglicherweise eine Verbesserung mit weniger lokalen Variablen sehen, ist, wenn dies die Arbeit für den Garbage Collector irgendwie reduziert. Aber das wird nicht der Fall sein, wenn Sie einfach auf die gleiche Variable zuweisen. Dazu müssten Sie ganze Tabellen oder Benutzerdaten (ohne Neuzuweisung) wiederverwenden. Mit anderen Worten, die Verwendung der gleichen Felder einer Tabelle, ohne dass ein ganz neues neu erstellt werden muss, kann in manchen Fällen hilfreich sein, aber es ist sehr unwahrscheinlich, dass die zur Referenzierung der Tabelle verwendete Variable die Performance beeinträchtigt.

+1

In der Tat. Wenn ich "local a" ansehe; für i = 1.100000000 tue a = i * 3 end 'Ich kann sehen, dass es gar nichts macht. Ich weiß, Compiler-Schreiber sind schlauer als ich, also wäre ich nicht überrascht, wenn ein C++ - Compiler so etwas in ein NOP optimieren würde. –

3

Alle lokalen Variablen werden bei der Kompilierung (load) Zeit erstellt und sind einfach Indizes in den lokalen Block des Funktionsaktivierungs-Datensatzes. Jedes Mal, wenn Sie eine local definieren, wird dieser Block um 1 erhöht. Jedes Mal, wenn der lexikalische Block beendet ist, schrumpft er zurück. Spitzenwert wird als Gesamtgröße verwendet:

function() 
    local a  -- current:1, peak:1 
    do 
     local x -- current:2, peak:2 
     local y -- current:3, peak:3 
    end 
        -- current:1, peak:3 
    do 
     local z -- current:2, peak:3 
    end 
end 

Die obige Funktion hat 3 lokale Schlitze (bestimmt bei load, nicht zur Laufzeit).

In Bezug auf Ihren Fall gibt es keinen Unterschied in der lokalen Blockgröße und darüber hinaus luac/5.1 erzeugt gleich Anzeigen (nur Indizes aus):

$ luac -l - 
local a; for i=1,100000000 do a = i * 3 end 
^D 
main <stdin:0,0> (7 instructions, 28 bytes at 0x7fee6b600000) 
0+ params, 5 slots, 0 upvalues, 5 locals, 3 constants, 0 functions 
     1  [1]  LOADK   1 -1 ; 1 
     2  [1]  LOADK   2 -2 ; 100000000 
     3  [1]  LOADK   3 -1 ; 1 
     4  [1]  FORPREP   1 1  ; to 6 
     5  [1]  MUL    0 4 -3 ; - 3  // [0] is a 
     6  [1]  FORLOOP   1 -2 ; to 5 
     7  [1]  RETURN   0 1 

vs

$ luac -l - 
for i=1,100000000 do local a = i * 3 end 
^D 
main <stdin:0,0> (7 instructions, 28 bytes at 0x7f8302d00020) 
0+ params, 5 slots, 0 upvalues, 5 locals, 3 constants, 0 functions 
     1  [1]  LOADK   0 -1 ; 1 
     2  [1]  LOADK   1 -2 ; 100000000 
     3  [1]  LOADK   2 -1 ; 1 
     4  [1]  FORPREP   0 1  ; to 6 
     5  [1]  MUL    4 3 -3 ; - 3  // [4] is a 
     6  [1]  FORLOOP   0 -2 ; to 5 
     7  [1]  RETURN   0 1 

// [n] -comments sind meine.

Verwandte Themen