2017-02-09 10 views
10

Я сделал свою домашнюю работу и нашел многократные заверения в том, что не имеет никакого значения в производительности независимо от того, объявляете ли вы свои переменные внутри или вне цикла for и фактически компилируете тот же MSIL. Но я все-таки возился с ним и обнаружил, что перемещение деклараций переменных внутри цикла действительно вызывает значительное и последовательное увеличение производительности.Могут ли переменные, объявленные внутри цикла for, влиять на производительность цикла?

Я написал небольшой тестовый класс консоли для измерения этого эффекта. Я инициализирую статический массив double[], и два метода выполняют на нем операции цикла, записывая результаты в статический массив double[]. Первоначально моими методами были те, с которыми я заметил разницу, а именно вычисление величины комплексного числа. Запустив их для элементов массив длиной 1000000 в 100 раз, я получил последовательно меньшее время выполнения для того, в котором переменные (6 double) находились внутри цикла: например, 32,83 ± 0,64 мс против 43 , 24 ± 0,45 мс на пожилой конфигурации с Intel Core 2 Duo @ 2,66 ГГц. Я попытался выполнить их в другом порядке, но это не повлияло на результаты.

Тогда я понял, что вычисления величины комплексного числа далеко от рабочего примера минимального и протестировали два гораздо более простые методы:

static void Square1() 
    { 
     double x; 

     for (int i = 0; i < buffer.Length; i++) { 
      x = items[i]; 
      buffer[i] = x * x; 
     } 
    } 


    static void Square2() 
    { 
     for (int i = 0; i < buffer.Length; i++) { 
      double x; 
      x = items[i]; 
      buffer[i] = x * x; 
     } 
    } 

С этим, результаты вышли в другую сторону: объявляя переменную вне петли показалось более благоприятным: 7.07 ± 0.43 мс для Square1() v 12.07 ± 0.51 мс для Square2().

Я не знаком с ILDASM, но я разобрал эти два метода, и единственное различие, кажется, инициализация локальных переменных:

 .locals init ([0] float64 x, 
     [1] int32 i, 
     [2] bool CS$4$0000) 

в Square1() v

 .locals init ([0] int32 i, 
     [1] float64 x, 
     [2] bool CS$4$0000) 

в Square2(). В соответствии с этим, что stloc.1 в одном, это stloc.0 в другом, и наоборот. В более длинном комплексном вычислении величины MSIL кодирует даже размер кода, и я видел stloc.s i в коде внешней декларации, где в коде внутренней декларации было stloc.0.

Итак, как это может быть? Я что-то забываю или это реальный эффект? Если это так, это может существенно повлиять на производительность длинных циклов, поэтому я думаю, что это заслуживает обсуждения.

Ваши мысли очень ценятся.

EDIT: Единственное, что я пропустил, это проверить его на нескольких компьютерах перед публикацией. Я запустил его на i5 сейчас, а результаты почти идентичны для двух методов. Приносим извинения за то, что вы опубликовали такое вводящее в заблуждение наблюдение.

+1

Хорошее исследование, вы наверняка заработаете над головой. – NicoRiff

+2

@NicoRiff: Действительно, это очень хорошо написанный вопрос. (К сожалению, хотя я думаю, что ответ тривиален.) – Bathsheba

+1

Я не могу ждать ответа @JonSkeet на этот вопрос – NicoRiff

ответ

5

Любой компилятор C#, достойный своей соли, будет выполнять такие микро-оптимизации для вас. Если это необходимо, пропустите переменную за пределами области действия.

Поэтому держите double x; внутри петли, если это возможно.

Лично, если items[i] - это простой доступ к массиву данных, тогда я бы написал buffer[i] = items[i] * items[i];. C и C++ оптимизировали бы это, но я не думаю, что C# (пока); ваша разборка подразумевает, что это не так.

+1

но почему? – NicoRiff

+0

Большое спасибо! Раньше я придерживался навязчивой привычки объявлять всю свою переменную в начале метода, но отныне буду думать дважды. Мое сообщение о возврате домой - это проверить обе договоренности, если я забочусь о производительности, потому что кажется, что оптимизация может работать в обоих направлениях. –

+1

Шумный ответ - «многолетний опыт говорит вам, что переменные с ограниченным охватом заканчиваются в полном беспорядке кодовой базы». – Bathsheba

1

Было бы интересно прокомментировать, что делает сборщик мусора для этих двух вариантов.

Я могу представить, что в первом случае переменная x не собирается во время цикла, потому что она объявлена ​​во внешней области.

Во втором случае все ручки на x будут удалены на каждой итерации.

Возможно, вы снова проведете тест с новыми C# 4.6 GC.TryStartNoGCRegion и GC.EndNoGCRegion, чтобы узнать, связано ли влияние производительности с GC.

Prevent .NET Garbage collection for short period of time

+0

Спасибо, это отличная идея. Я хотел, чтобы он был протестирован уже, но на данный момент у меня нет доступа к .NET 4.6. SharpDevelop, похоже, не поддерживает его. Я попытаюсь обновить свои инструменты и вернуться к вопросу. –

+1

Я сомневаюсь, что это имеет какое-то отношение к GC. 'double' - это тип значения и в этом случае будет выделен стек. Он не генерирует мусор для очистки. – Kyle

+2

Эрик Липперт дает отличную информацию об этом на http://stackoverflow.com/a/14043763/526724 –