Есть ли разница в производительности между ++ i и i ++ в C #? - PullRequest
46 голосов
/ 22 января 2009

Есть ли разница в производительности между использованием чего-то вроде

for(int i = 0; i < 10; i++) { ... }

и

for(int i = 0; i < 10; ++i) { ... }

или компилятор может оптимизировать таким образом, чтобы они были одинаково быстры в случае, когда они функционально эквивалентны?

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

Ответы [ 9 ]

36 голосов
/ 22 января 2009

В этом случае нет разницы в сгенерированном промежуточном коде для ++ i и i ++. Учитывая эту программу:

class Program
{
    const int counter = 1024 * 1024;
    static void Main(string[] args)
    {
        for (int i = 0; i < counter; ++i)
        {
            Console.WriteLine(i);
        }

        for (int i = 0; i < counter; i++)
        {
            Console.WriteLine(i);
        }
    }
}

Сгенерированный код IL одинаков для обоих циклов:

  IL_0000:  ldc.i4.0
  IL_0001:  stloc.0
  // Start of first loop
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0010
  IL_0006:  ldloc.0
  IL_0007:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_000c:  ldloc.0
  IL_000d:  ldc.i4.1
  IL_000e:  add
  IL_000f:  stloc.0
  IL_0010:  ldloc.0
  IL_0011:  ldc.i4     0x100000
  IL_0016:  blt.s      IL_0006
  // Start of second loop
  IL_0018:  ldc.i4.0
  IL_0019:  stloc.0
  IL_001a:  br.s       IL_0026
  IL_001c:  ldloc.0
  IL_001d:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0022:  ldloc.0
  IL_0023:  ldc.i4.1
  IL_0024:  add
  IL_0025:  stloc.0
  IL_0026:  ldloc.0
  IL_0027:  ldc.i4     0x100000
  IL_002c:  blt.s      IL_001c
  IL_002e:  ret

Тем не менее, вполне возможно (хотя и крайне маловероятно), что JIT-компилятор может выполнять некоторые оптимизации в определенных контекстах, которые будут благоприятствовать одной версии над другой. Однако, если такая оптимизация существует, она может повлиять только на последнюю (или, возможно, первую) итерацию цикла.

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

7 голосов
/ 22 января 2009

Если вы задаете этот вопрос, вы пытаетесь решить не ту проблему.

Первый вопрос, который нужно задать: «Как повысить удовлетворенность клиентов своим программным обеспечением, сделав его быстрее?» и ответ почти никогда не «используй ++ i вместо i ++» или наоборот.

Из поста Coding Horror " Аппаратные средства дешевы, программисты дорогие ":

Правила оптимизации:
Правило 1: не делай этого.
Правило 2 (только для экспертов): пока не делайте этого.
- М.А. Джексон

Я прочитал правило 2 как «сначала пишите чистый, понятный код, который соответствует потребностям вашего клиента, а затем ускоряйте его там, где он слишком медленный». Маловероятно, что ++i против i++ будет решением.

7 голосов
/ 22 января 2009

Ах ... Открыто снова. ХОРОШО. Вот сделка.

ILDASM - это начало, но не конец. Ключ: что сгенерирует JIT для ассемблерного кода?

Вот что вы хотите сделать.

Возьмите пару примеров того, на что вы пытаетесь взглянуть. Очевидно, вы можете настроить время на них, если хотите, но я предполагаю, что вы хотите знать больше.

Вот что не очевидно. Компилятор C # генерирует некоторые последовательности MSIL, которые неоптимальны во многих ситуациях. JIT настроен на то, чтобы справиться с этим и причудами других языков. Проблема: настроены только «причуды», замеченные кем-то.

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

Вы НЕ хотите запускать код под отладчиком, иначе JIT сгенерирует неоптимизированный код - и, похоже, вы хотите знать, как он будет вести себя в реальной среде. JIT делает это, чтобы максимизировать отладочную информацию и минимизировать текущее местоположение источника от «прыжков вокруг». Никогда не запускайте тестирование отладчика.

OK. Поэтому, как только код был запущен один раз (то есть: JIT сгенерировал код для него), затем подключите отладчик во время сна (или чего-то еще). Затем посмотрите на x86 / x64, сгенерированный для двух подпрограмм.

Мой инстинкт говорит мне, что если вы используете ++ i / i ++, как вы описали, то есть в автономном выражении, где результат rvalue не используется повторно, разницы не будет. Но разве не будет забавно пойти узнать и увидеть все аккуратные вещи! :)

5 голосов
/ 22 января 2009

Как показал Джим Мишель , компилятор сгенерирует идентичный MSIL для двух способов написания цикла for.

Но это так: нет никаких оснований для спекуляций относительно JIT или для измерения скорости. Если две строки кода генерируют идентичный MSIL, они не только будут работать одинаково, но и будут фактически идентичны.

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

4 голосов
/ 22 января 2009

Ребята, ребята, «ответы» для C и C ++.

C # - другое животное.

Используйте ILDASM для просмотра скомпилированного вывода, чтобы проверить, есть ли разница в MSIL.

3 голосов
/ 22 января 2009

Имеете в виду конкретный кусок кода и выпуск CLR? Если так, отметьте это. Если нет, забудь об этом. Микрооптимизация и все такое ... Кроме того, вы даже не можете быть уверены, что разная версия CLR даст одинаковый результат.

2 голосов
/ 22 января 2009

В дополнение к другим ответам, может быть разница , если ваш i не является int. В C ++ , если это объект класса, у которого перегружены операторы ++() и ++(int), то это может иметь значение и, возможно, побочный эффект. В этом случае производительность ++i должна быть лучше (зависит от реализации).

0 голосов
/ 22 января 2009
  static void Main(string[] args) {
     var sw = new Stopwatch(); sw.Start();
     for (int i = 0; i < 2000000000; ++i) { }
     //int i = 0;
     //while (i < 2000000000){++i;}
     Console.WriteLine(sw.ElapsedMilliseconds);

Среднее из 3 прогонов:
для с i ++: 1307 для с ++ я: 1314

в то время как с i ++: 1261 в то время как с ++ я: 1276

Это Celeron D с частотой 2,53 ГГц. Каждая итерация занимала около 1,6 такта процессора. Это либо означает, что ЦП выполнял более 1 инструкции за цикл, либо что JIT-компилятор развернул циклы. Разница между i ++ и ++ i составляла всего 0,01 такта процессора за итерацию, вероятно, вызванная службами ОС в фоновом режиме.

0 голосов
/ 22 января 2009

Согласно этому ответу , i ++ использует одну инструкцию процессора больше, чем ++ i. Но приводит ли это к разнице в производительности, я не знаю.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...