Вопрос

Меня всегда интересовало, имеет ли вообще какое-либо значение (производительность) объявление выбрасываемой переменной перед циклом, а не повторное объявление внутри цикла?А (совершенно бессмысленно) пример на Java:

а) объявление перед циклом:

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);
}

Какая из них лучше, а или б?

Я подозреваю, что повторное объявление переменной (пример б) создает больше накладных расходов в теории, но компиляторы достаточно умны, так что это не имеет значения.Пример б имеет то преимущество, что он более компактен и ограничивает область применения переменной местом ее использования.Тем не менее, я склонен писать код в соответствии с примером а.

Редактировать: Меня особенно интересует случай Java.

Это было полезно?

Решение

Как лучше, а или б?

С точки зрения производительности вам придется ее измерить.(И, на мой взгляд, если можно измерить разницу, то компилятор не очень хорош).

С точки зрения технического обслуживания, б лучше.Объявляйте и инициализируйте переменные в одном и том же месте, в максимально узкой области видимости.Не оставляйте зияющую дыру между объявлением и инициализацией и не загрязняйте пространства имен, которые вам не нужны.

Другие советы

Ну, я запускал ваши примеры A и B по 20 раз каждый, повторяя 100 миллионов раз (JVM - 1.5.0).

А:среднее время выполнения:0,074 секунды

Б:среднее время выполнения:0,067 секунды

К моему удивлению, Б оказался немного быстрее.Какими бы быстрыми ни были сейчас компьютеры, трудно сказать, сможете ли вы точно это измерить.Я бы тоже закодировал это способом A, но я бы сказал, что это не имеет особого значения.

Это зависит от языка и конкретного использования.Например, в C# 1 это не имело никакого значения.В C# 2, если локальная переменная захватывается анонимным методом (или лямбда-выражением в C# 3), это может иметь очень существенное значение.

Пример:

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 переменная.

Ниже приведено то, что я написал и скомпилировал в .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);
}

Это то, что я получаю от .NET-отражатель когда КИЛ отображается обратно в код.

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;
}

Я скомпилировал эти коды с помощью gcc-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 

Это точно такой же результат.разве это не доказательство того, что два кода производят одно и то же?

Я бы всегда использовал A (вместо того, чтобы полагаться на компилятор), а также мог бы переписать его так:

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

Это по-прежнему ограничивает intermediateResult в область действия цикла, но не объявляется повторно во время каждой итерации.

Это зависит от языка - IIRC C# оптимизирует это, поэтому нет никакой разницы, но JavaScript (например) каждый раз будет выполнять всю операцию по распределению памяти.

На мой взгляд, b — лучшая структура.В случае a последнее значение промежуточного результата сохраняется после завершения цикла.

Редактировать:Это не имеет большого значения для типов значений, но ссылочные типы могут быть несколько весомыми.Лично мне нравится, чтобы переменные разыменовывались как можно скорее для очистки, и b делает это за вас:

Я подозреваю, что некоторые компиляторы могли бы оптимизировать оба кода, но, конечно, не все.Так что я бы сказал, что тебе лучше первое.Единственная причина для последнего — если вы хотите убедиться, что объявленная переменная используется. только внутри вашего цикла.

Как правило, я объявляю переменные в самой внутренней области видимости.Итак, если вы не используете промежуточный результат вне цикла, я бы выбрал B.

Коллега предпочитает первую форму, говоря, что это оптимизация, и предпочитает повторно использовать объявление.

Я предпочитаю второй (и попробую убедить коллегу!;-))), прочитав это:

  • Это уменьшает объем переменных до тех мест, где они необходимы, и это хорошо.
  • Java достаточно оптимизируется, чтобы не оказать существенного влияния на производительность.IIRC, возможно, вторая форма еще быстрее.

В любом случае, это попадает в категорию преждевременной оптимизации, которая зависит от качества компилятора и/или JVM.

В C# есть разница, если вы используете переменную в лямбде и т. д.Но в целом компилятор будет делать то же самое, если предположить, что переменная используется только внутри цикла.

Учитывая, что они по сути одинаковы:Обратите внимание, что версия b делает для читателей гораздо более очевидным, что переменная не может быть использована после цикла.Кроме того, версию b гораздо легче рефакторить.В версии a сложнее извлечь тело цикла в отдельный метод. Более того, версия b уверяет вас, что такой рефакторинг не имеет побочных эффектов.

Следовательно, версия а меня безмерно раздражает, потому что в ней нет никакой пользы и из-за нее гораздо сложнее рассуждать о коде...

Ну, вы всегда можете сделать для этого возможность:

{ //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();
}

Тогда вы создаете только одну ссылку и каждый раз назначаете ей новый объект.Конечно, может потребоваться немного больше времени, чтобы он вышел за пределы области видимости, но тогда остается только одна висячая ссылка, с которой нужно иметь дело.

Я думаю, что это зависит от компилятора и сложно дать общий ответ.

Моя практика следующая:

  • если тип переменной простой (целое, двойное, ...) Я предпочитаю вариант б (внутри).
    Причина: уменьшение объема переменной.

  • если тип переменной не простой (наподобие class или struct) Я предпочитаю вариант а (снаружи).
    Причина: уменьшение количества вызовов ctor-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 миллиарду раз каждую.external() заняло 65 миллисекунд.Inside() заняло 1,5 секунды.

А) это более безопасный выбор, чем Б).........Представьте, что вы инициализируете структуру в цикле, а не 'int' или 'float', тогда что?

нравиться

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» является более безопасным выбором, в то время как «B» уязвим для накопления памяти, особенно при работе с библиотеками с закрытым исходным кодом. Вы можете проверить это с помощью инструмента «Valgrind» в Linux, в частности, подинструмента «Helgrind».

Это интересный вопрос.По моему опыту, при обсуждении этого вопроса при разработке кода следует учитывать главный вопрос:

Есть ли причина, по которой переменная должна быть глобальной?

Имеет смысл объявлять переменную только один раз глобально, а не много раз локально, поскольку это лучше организует код и требует меньше строк кода.Однако, если ее нужно объявить локально только в одном методе, я бы инициализировал ее в этом методе, чтобы было ясно, что переменная относится исключительно к этому методу.Будьте осторожны и не вызывайте эту переменную вне метода, в котором она инициализирована, если вы выберете последний вариант — ваш код не поймет, о чем вы говорите, и сообщит об ошибке.

Также, в качестве примечания, не дублируйте имена локальных переменных в разных методах, даже если их цели почти идентичны;это просто сбивает с толку.

Я тестировал JS с Node 4.0.0, если кому-то интересно.Объявление вне цикла привело к улучшению производительности примерно на 0,5 мс в среднем за 1000 испытаний со 100 миллионами итераций цикла на испытание.Итак, я скажу: «Давай, напиши это наиболее читабельным/удобным в сопровождении способом, то есть B», по моему мнению.Я бы поместил свой код в скрипку, но использовал модуль Node с повышенной производительностью.Вот код:

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) Таким образом, лучшим практическим правилом является любое объявление вне итерации for.

Попробовал то же самое в Go и сравнил вывод компилятора, используя 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 и в обоих случаях получил такой же 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);
}

Нулевая работа декомпилирована

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

Даже если я знаю, что мой компилятор достаточно умен, я не хочу на него полагаться и буду использовать вариант а).

Вариант б) имеет для меня смысл только в том случае, если вам крайне необходимо сделать промежуточный результат недоступен после тела цикла.Но я все равно не могу себе представить такой отчаянной ситуации....

РЕДАКТИРОВАТЬ: Джон Скит сделал очень хорошее замечание, показав, что объявление переменных внутри цикла может иметь реальное семантическое значение.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top