Diferença entre declarar variáveis ??antes ou em loop?
-
03-07-2019 - |
Pergunta
Eu sempre me perguntei se, em geral, declarar uma variável descartável antes de um loop, em oposição a várias vezes dentro do loop, faz alguma diferença (desempenho)? A (bastante inútil) exemplo em Java:
a) declaração antes de loop:
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);
}
Qual é melhor, a ou b ?
Eu suspeito que repetiu declaração da variável (exemplo b ) cria mais sobrecarga em teoria , mas isso compiladores são suficientes inteligente para que ele não importa. Exemplo
Editar:. Estou especialmente interessado no caso Java
Solução
O que é melhor, a ou b ?
A partir de uma perspectiva de desempenho, você tem que medir isso. (E, na minha opinião, se você pode medir a diferença, o compilador não é muito bom).
Do ponto de vista de manutenção, b é melhor. Declarar e variáveis ??inicializar no mesmo lugar, no âmbito mais estreito possível. Não deixe um buraco entre a declaração ea inicialização, e fazer namespaces Não polua você não precisa.
Outras dicas
Bem, eu corri seus exemplos A e B 20 vezes cada um, looping 100 milhões de vezes (JVM - 1.5.0).
A: tempo médio de execução: 0,074 seg
B: tempo médio de execução: 0,067 seg
Para minha surpresa B foi um pouco mais rápido. Tão rápido quanto os computadores são agora é difícil dizer se você pudesse medir com precisão este. Gostaria de codificá-lo a um caminho tão bem, mas eu diria que realmente não importa.
Depende da linguagem e o uso exato. Por exemplo, em C # 1 não fazia diferença. Em C # 2, se a variável local é capturado por um método anônimo (ou expressão lambda em C # 3) ele pode fazer uma diferença muito signficant.
Exemplo:
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();
}
}
}
Output:
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
A diferença é que todas as ações capturar a mesma variável outer
, mas cada um tem sua própria variável inner
separado.
O seguinte é o que eu escrevi e compilado no .NET.
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);
}
Isto é o que eu recebo de .NET Reflector quando CIL é processado de volta em código.
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);
}
Assim, ambos olhar exatamente o mesmo após a compilação. Em linguagens de código gerenciado é convertido em CL / código de byte e em tempo de execução é convertida em linguagem de máquina. Assim, em linguagem de máquina um duplo não pode mesmo ser criado na pilha. Pode ser apenas um registo como código refletir que é uma variável temporária para a função WriteLine
. Existem regras integrais de otimização conjunto apenas para loops. Assim, o indivíduo médio não deve se preocupar com isso, especialmente em idiomas gerenciados. Há casos em que você a otimizar pode gerenciar código, por exemplo, se você tem que concatenar um grande número de cordas usando apenas string a; a+=anotherstring[i]
vs usando StringBuilder
. Não é muito grande diferença no desempenho entre ambos. Há uma série de casos em que o compilador não pode otimizar o seu código, porque ele não consegue descobrir o que se pretende em um escopo maior. Mas pode muito bem otimizar coisas básicas para você.
Esta é uma pegadinha no VB.NET. O resultado Visual Basic não vai reinicializar a variável neste exemplo:
For i as Integer = 1 to 100
Dim j as Integer
Console.WriteLine(j)
j = i
Next
' Output: 0 1 2 3 4...
Isto irá imprimir 0 pela primeira vez (variáveis ??Visual Basic têm valores padrão quando declarou!) Mas i
cada tempo depois disso.
Se você adicionar um = 0
, porém, você recebe o que você poderia esperar:
For i as Integer = 1 to 100
Dim j as Integer = 0
Console.WriteLine(j)
j = i
Next
'Output: 0 0 0 0 0...
Eu fiz um teste simples:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
vs
for (int i = 0; i < 10; i++) {
int b = i;
}
Eu compilei estes códigos com gcc - 5.2.0. E então eu desmontou o main () destes dois códigos e isso é o resultado:
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
vs
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
Quais exaclty o mesmo resultado asm. não é uma prova de que os dois códigos de produzir a mesma coisa?
Eu sempre utilizar A (em vez de depender do compilador) e também pode reescrever a:
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
Este ainda restringe intermediateResult
ao escopo do loop, mas não redeclare durante cada iteração.
É a linguagem dependente -. IIRC C # otimiza isso, então não há qualquer diferença, mas JavaScript (por exemplo) vai fazer toda a alocação de memória shebang cada vez
Na minha opinião, b é a melhor estrutura. Em um, o último valor de varas intermediateResult ao redor após o seu ciclo está terminado.
Edit: Isso não faz muita diferença com tipos de valor, mas os tipos de referência pode ser um pouco pesado. Pessoalmente, eu como variáveis ??a serem desreferenciado o mais rápido possível para limpeza, e b faz isso para você,
Eu suspeito que alguns compiladores podem otimizar tanto a ser o mesmo código, mas certamente não todos. Então eu diria que você é melhor fora com o primeiro. A única razão para este último é, se você quiser garantir que a variável declarada é utilizada única dentro de seu loop.
Como regra geral, eu declaro meus variáveis ??no escopo mais interna possível. Então, se você não está usando intermediateResult fora do circuito, então eu iria com B.
Um colega de trabalho prefere a primeira forma, dizendo que é uma otimização, preferindo voltar a usar uma declaração.
Eu prefiro o segundo (! E tentar convencer o meu colega de trabalho ;-)), depois de ter lido que:
- Reduz escopo de variáveis ??para onde eles são necessários, o que é uma coisa boa.
- Java otimiza o suficiente para fazer nenhuma diferença significativa no desempenho. IIRC, talvez a segunda forma é ainda mais rápido.
De qualquer forma, ele cai na categoria de otimização prematura que confiar na qualidade do compilador e / ou JVM.
Há uma diferença em C # se você estiver usando a variável em um lambda, etc. Mas, em geral, o compilador vai fazer basicamente a mesma coisa, supondo que a variável é usada apenas dentro do loop.
Tendo em conta que eles são basicamente os mesmos: Note-se que a versão b torna muito mais evidente para os leitores que a variável não é, e não pode, ser usado após o loop. Além disso, versão b é muito mais facilmente reformulado. É mais difícil de extrair o corpo do laço em seu próprio método na versão a. Além disso, versão B garante que não há nenhum efeito colateral a tal refatoração.
Assim, uma versão irrita-me para nenhum fim, porque não há nenhum benefício para ele e isso torna muito mais difícil de raciocinar sobre o código ...
Bem, você sempre pode fazer um espaço para que:
{ //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);
}
}
Desta forma você só declarar a variável uma vez, e ele vai morrer quando sair do loop.
Eu sempre pensei que, se você declarar suas variáveis ??dentro de seu loop, então você está desperdiçando memória. Se você tem algo parecido com isto:
for(;;) {
Object o = new Object();
}
Em seguida, não só a necessidade objeto a ser criado para cada iteração, mas é preciso que haja uma nova referência atribuída para cada objeto. Parece que se o coletor de lixo é lenta, então você vai ter um monte de balançando referências que precisam ser limpos.
No entanto, se você tem o seguinte:
Object o;
for(;;) {
o = new Object();
}
Em seguida, você está criando apenas uma única referência e atribuir um novo objeto a ele cada vez. Claro, isso pode demorar um pouco mais para que ele vá fora do escopo, mas depois há apenas uma referência pendendo para lidar com eles.
Eu acho que depende do compilador e é difícil dar uma resposta geral.
A minha prática é o seguinte:
-
Se tipo de variável é simples (int, double, ...) Eu prefiro variante b (no interior).
Motivo: reduzindo âmbito da variável. -
Se tipo de variável não é simples (algum tipo de
class
oustruct
) Eu prefiro variante a (fora).
Motivo:. reduzindo o número de chamadas ctor-dtor
A partir de uma perspectiva de desempenho, fora é (muito) melhor.
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;
}
}
Eu executada ambas as funções 1 bilhão de vezes cada. fora () levou 65 milissegundos. dentro () levou 1,5 segundos.
A) é uma aposta segura que B) ......... Imagine se você está inicializando estrutura em malha em vez de 'int' ou 'float' e depois?
como
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?
}
Você certamente obrigado a enfrentar problemas com vazamentos de memória !. Por isso eu acredito 'A' é aposta mais segura, enquanto 'B' é vulnerável à acumulação de memória esp trabalhando fonte próxima libraries.You pode verificar usinng Ferramenta 'Valgrind' no Linux especificamente sub ferramenta 'Helgrind'.
É uma pergunta interessante. Da minha experiência, há uma questão fundamental a considerar quando você debater este assunto para um código:
Existe alguma razão para que a variável que precisa ser global?
Não faz sentido apenas para declarar a variável uma vez, globalmente, ao contrário de muitas vezes localmente, porque é melhor para organizar o código e requer menos linhas de código. No entanto, se ele só precisa ser declarado localmente dentro de um método, eu inicializá-lo nesse método, por isso é claro que a variável é exclusivamente relevante para esse método. Tenha cuidado para não chamar esta variável fora do método em que é inicializado, se você escolher a última opção -. Seu código não vai saber o que você está falando e se reportará um erro
Além disso, como uma nota lateral, não duplicar nomes de variáveis ??locais entre diferentes métodos, mesmo que seus fins são quase idênticas; ele só fica confuso.
Eu testei para JS com Nó 4.0.0 Se alguém estiver interessado. Declarando fora do loop resultou numa melhoria do desempenho .5 ms ~, em média, mais de 1000 ensaios com 100 milhões de iterações em cada experiência de ansa. Então eu vou dizer vá em frente e escrevê-lo da forma mais legível / passível de manutenção que é B, imo. Eu colocaria meu código em um violino, mas eu usei o módulo Node desempenho de agora. Aqui está o código:
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)
Esta é a melhor forma
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1), desta forma declarada uma vez tempo, tanto variável, e não para cada ciclo. 2) a atribuição-lo de fatser thean tudo outra opção. 3) Portanto, a regra bestpractice é qualquer declaração fora da iteração para.
tentou a mesma coisa em Go, e comparou a saída do compilador usando go tool compile -S
com go 1.9.4
Zero diferença, de acordo com a saída do integrador.
Eu tinha essa mesma pergunta por um longo tempo. Então, eu testei uma peça ainda mais simples de código.
Conclusão:. Para Nesses casos não é NÃO diferença de desempenho
caso circuito Fora
int intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i+2;
System.out.println(intermediateResult);
}
Dentro caso de loop
for(int i=0; i < 1000; i++){
int intermediateResult = i+2;
System.out.println(intermediateResult);
}
Eu verifiquei o arquivo compilado em decompiler do IntelliJ e para ambos os casos, eu tenho o mesma Test.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
I código também desmontado, tanto para o caso, utilizando o método apresentado neste resposta . Vou mostrar apenas as partes relevantes para a resposta ??p>
caso circuito Fora
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;
Dentro caso de loop
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;
Se você prestar atenção, apenas o Slot
atribuído a i
e intermediateResult
em LocalVariableTable
é trocado como um produto da sua ordem de aparecimento. A mesma diferença de ranhura é reflectida em outras linhas de código.
- Nenhuma operação adicional está sendo executada
-
intermediateResult
ainda é uma variável local em ambos os casos, portanto, não há tempo de acesso diferença.
bônus
Os compiladores fazer uma tonelada de otimização, dê uma olhada no que acontece neste caso.
Zero caso de trabalho
for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}
trabalho Zero decompiled
for(int i = 0; i < 1000; ++i) {
System.out.println(i);
}
Mesmo se eu sei que meu compilador é inteligente o suficiente, eu não vou gostar de contar com ele, e usará o a) variante.
A b) variante faz sentido para mim se você precisa desesperadamente fazer o intermediateResult não disponível depois que o corpo do laço. Mas eu não posso imaginar tal situação desesperada, de qualquer maneira ....
EDIT:. Jon Skeet fez um ponto muito bom, mostrando que declaração da variável dentro de um loop pode fazer uma diferença semântica real