2009-12-31 4 views
3

Если у меня есть подобный кодКак компилятор C# оптимизирует фрагмент кода?

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

ли компилятор instantinate iTemp 10 раз?

Или это оптимизировано?

Я имею в виду, если я переписать цикл как

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

Будет ли это быстрее?

+2

'int's не являются объектами, поэтому создание их - это, вероятно, одна или две инструкции. Может быть, объект станет лучшим примером. –

+0

Мне нравится второй подход.Зачем вам нужна эта дополнительная переменная? Кроме того, добавьте несколько пробелов в цикл for! –

+0

И, наконец, всегда полезно сравнить полученный ИЛ и ПРОФИЛЬ! –

ответ

20

Используя reflector, вы можете просмотреть IL, сгенерированный компилятором C#.

.method private hidebysig static void Way1() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 i) 
    L_0000: ldc.i4.0 
    L_0001: stloc.0 
    L_0002: br.s L_0008 
    L_0004: ldloc.0 
    L_0005: ldc.i4.1 
    L_0006: add 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldc.i4.s 10 
    L_000b: blt.s L_0004 
    L_000d: ret 
} 

.method private hidebysig static void Way2() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] int32 i) 
    L_0000: ldc.i4.0 
    L_0001: stloc.0 
    L_0002: br.s L_0008 
    L_0004: ldloc.0 
    L_0005: ldc.i4.1 
    L_0006: add 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldc.i4.s 10 
    L_000b: blt.s L_0004 
    L_000d: ret 
} 

Они точно такие же, что и отсутствие разницы в производительности, когда вы объявляете iTemp.

+1

В конкретном отрывке короткого кода, который он показал, это правильно, но в более крупном методе он может отличаться. Основное отличие состоит в том, что во втором примере кода переменная iTemp будет доступна после цикла, тогда как в первом случае она не будет. так что «или иначе» не совсем правильно. –

+0

@ Lasse V. Karlsen: Правильно и исправлено. –

1

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

Это простая форма подъема петли.

3

Если вам действительно интересно, как CSC (компилятор C#) обрабатывает ваш код, вы можете захотеть сыграть с LINQPad - он позволяет, помимо прочего, вводить короткие выражения или программы C# и смотреть на полученный IL (байт-код CLR).

4

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

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

Try:

var a = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

var q = a.AsEnumerable(); 
int iTemp; 
for(int i=0;i<10;i++) 
{ 
    iTemp = i; 
    q = q.Where(x => x <= iTemp); 
} 

Console.WriteLine(string.Format("{0}, count is {1}", 
    string.Join(":", q.Select(x => x.ToString()).ToArray()), 
    q.Count())); 

и

var a = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

var q = a.AsEnumerable(); 
for(int i=0;i<10;i++) 
{ 
    var iTemp = i; 
    q = q.Where(x => x <= iTemp); 
} 

Console.WriteLine(string.Format("{0}, count is {1}", 
    string.Join(":", q.Select(x => x.ToString()).ToArray()), 
    q.Count())); 
+0

У меня была небольшая ошибка транскрипции - оставлено начальное значение как -1 и каким-то образом ввернуто string.Join() при вставке кода. Если у вас были ошибки компиляции, попробуйте новые версии. – tvanfosson

3

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

Рассмотрим:

int Func(int a, int b, int c) 
{ 
    int x = a * 2; 
    int y = b * 3; 
    int z = c * 4; 
    return x + y + z; 
} 

Игнорируя тот факт, что это может быть легко оптимизированы для возврата (а * 2) + (б * 3) + (с * 4), компилятор будет увидеть три локальные переменные и выделить место для трех локальных переменных.

Если у меня есть это:

int Func(int a, int b, int c) 
{ 
    int x = a * 2; 
    { 
     int y = b * 3; 
     { 
      int z = c * 4; 
      { 
       return x + y + z; 
      } 
     } 
    } 
} 

Это все те же 3 локальные переменные - только в различных областях. Цикл for - это не что иное, как блок областей с небольшим кодом клея, чтобы заставить его работать.

Теперь рассмотрим это:

int Func(int a, int b, int c) 
{ 
    int x = a * 2; 
    { 
     int y = b * 3; 
     x += y; 
    } 
    { 
     int z = c * 4; 
     x += z; 
    } 
    return x; 
} 

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

В качестве примечания, компилятор C# в VS2008 в выпуске даже не выполняет простейшие сокращения прочности. IL для первой версии это:

L_0000: ldarg.0 
L_0001: ldc.i4.2 
L_0002: mul 
L_0003: stloc.0 
L_0004: ldarg.1 
L_0005: ldc.i4.3 
L_0006: mul 
L_0007: stloc.1 
L_0008: ldarg.2 
L_0009: ldc.i4.4 
L_000a: mul 
L_000b: stloc.2 
L_000c: ldloc.0 
L_000d: ldloc.1 
L_000e: add 
L_000f: ldloc.2 
L_0010: add 
L_0011: ret 

в то время как я ожидал увидеть это:

L_0000: ldarg.0 
L_0001: ldc.i4.2 
L_0002: mul 
L_0003: ldarg.1 
L_0004: ldc.i4.3 
L_0005: mul 
L_0006: add 
L_0007: ldarg.2 
L_0008: ldc.i4.4 
L_0009: mul 
L_000a: add 
L_000b: ret 
+0

Это работа джиттера, чтобы делать такие оптимизации. –

+0

Действительно? Я бы подумал, что это то, что лучше всего обрабатывать, когда у вас есть дерево разбора в ваших жадных лапках. – plinth

0

Многие люди предоставили вам IL, чтобы показать вам, что ваши два фрагмента кода эффективно то же самое с точки зрения производительности. На самом деле нет необходимости идти на этот уровень детализации, чтобы понять, почему это так. Подумайте об этом с точки зрения call stack.

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

В обоих случаях то, что видит компилятор, является локальным с именем iTemp, поэтому, когда он выделяет пространство в стеке для локалей, он будет выделять 32 бита для хранения iTemp. Для компилятора не имеет значения, что в двух фрагментах кода iTemp имеют разный объем; компилятор будет применять это, просто не позволяя ссылаться на iTemp за пределами цикла for в первом фрагменте. То, что он будет делать, это выделить это пространство один раз (в начале метода) и повторно использовать пространство по мере необходимости во время цикла в первом фрагменте.

0

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

Давайте рассмотрим предыдущий пример:

static int Func(int a, int b, int c) 
{ 
    int x = a * 2; 
    int y = b * 3; 
    int z = c * 4; 
    return x + y + z; 
} 

излучаемого IL от 3.5 компилятора с оптимизациями поддержкой выглядит следующим образом:

.method private hidebysig static int32 Func(int32 a, 
              int32 b, 
              int32 c) cil managed 
{ 
    // Code size  18 (0x12) 
    .maxstack 2 
    .locals init (int32 V_0, 
      int32 V_1, 
      int32 V_2) 
    IL_0000: ldarg.0 
    IL_0001: ldc.i4.2 
    IL_0002: mul 
    IL_0003: stloc.0 
    IL_0004: ldarg.1 
    IL_0005: ldc.i4.3 
    IL_0006: mul 
    IL_0007: stloc.1 
    IL_0008: ldarg.2 
    IL_0009: ldc.i4.4 
    IL_000a: mul 
    IL_000b: stloc.2 
    IL_000c: ldloc.0 
    IL_000d: ldloc.1 
    IL_000e: add 
    IL_000f: ldloc.2 
    IL_0010: add 
    IL_0011: ret 
} // end of method test::Func 

не очень оптимальное право? Я компилирую его в исполняемый файл, вызывая его из простого метода Main, и компилятор не вставляет его и не делает никаких оптимизаций.

Так что же происходит во время выполнения?

JIT-это фактически встраивание вызов Func() и производить гораздо лучший код, чем вы можете себе представить, если смотреть на основе стека IL вверх выше:

mov  edx,dword ptr [rbx+10h] 
mov  eax,1 
cmp  rax,rdi 
jae  000007ff`00190265 

mov  eax,dword ptr [rbx+rax*4+10h] 
mov  ecx,2 
cmp  rcx,rdi 
jae  000007ff`00190265 

mov  ecx,dword ptr [rbx+rcx*4+10h] 
add  edx,edx 
lea  eax,[rax+rax*2] 
shl  ecx,2 
add  eax,edx 
lea  esi,[rax+rcx] 
Смежные вопросы