Страницы

Поиск по вопросам

понедельник, 11 февраля 2019 г.

Разный IL для инициализации свойств со значениями

Рассматриваю в Linqpad'е IL-код простенького итератора:
void Main() { var a = new A(); a.Skip(2).Take(5).Dump(); }
public class A : IEnumerable { public IEnumerator GetEnumerator() { return new B(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
public class B : IEnumerator { public int Current { get { return 123; } }
object IEnumerator.Current { get { return this.Current; } }
public void Dispose() {}
public bool MoveNext() { return true; }
public void Reset() {} }
Любопытно сравнение кода, если воспользоваться новой возможностью c# 6 Инициализация свойств со значениями и заменить:
public int Current { get { return 123; } }
на:
public int Current => 123;
Вот что будет в IL для первого случая:
B.get_Current: IL_0000: nop IL_0001: ldc.i4.s 7B IL_0003: stloc.0 IL_0004: br.s IL_0006 IL_0006: ldloc.0 IL_0007: ret
и второго:
B.get_Current: IL_0000: ldc.i4.s 7B IL_0002: ret
Обычно новые фичи языка утяжеляют конструкции, но тут обратный случай: код чище и быстрее.
Я не понимаю, что помешало комплятору в первом случае выкинуть явно лишние команды. В обоих случаях Current – свойство get-only, простая константа.
При этом если рассматривать классическую замену, то происходят абсолютно аналогичные вещи.
Для
void Main() { var abc = new Abc {}; }
public class Abc { private int _id = 123; // backing field public int Id { get { return _id; } set { _id = value; } } }
будет сгенерирован такой код:
IL_0000: nop IL_0001: newobj UserQuery+Abc..ctor IL_0006: stloc.0 // abc IL_0007: ret
Abc.get_Id: IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld UserQuery+Abc._id IL_0007: stloc.0 IL_0008: br.s IL_000A IL_000A: ldloc.0 IL_000B: ret
Abc.set_Id: IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld UserQuery+Abc._id IL_0008: ret
Abc..ctor: IL_0000: ldarg.0 IL_0001: ldc.i4.s 7B IL_0003: stfld UserQuery+Abc._id IL_0008: ldarg.0 IL_0009: call System.Object..ctor IL_000E: nop IL_000F: ret
А для
public int Id { get; set; } = 123;
оптимизированный:
IL_0000: nop IL_0001: newobj UserQuery+Abc..ctor IL_0006: stloc.0 // abc IL_0007: ret
Abc.get_Id: IL_0000: ldarg.0 IL_0001: ldfld UserQuery+Abc.k__BackingField IL_0006: ret
Abc.set_Id: IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld UserQuery+Abc.k__BackingField IL_0007: ret
Abc..ctor: IL_0000: ldarg.0 IL_0001: ldc.i4.s 7B IL_0003: stfld UserQuery+Abc.k__BackingField IL_0008: ldarg.0 IL_0009: call System.Object..ctor IL_000E: nop IL_000F: ret
Вообще, что делает соптимизированный участок кода, какую роль он выполняет в старой версии кода?
Выигрыш в коде получается копеечный, но выходит что в целях микрооптимизаций (если вдруг дойдёт до них) предпочтительнее использовать новую конструкцию.


Ответ

Вообще вопрос сам по себе достаточно бессмысленный.
Вы сравниваете результат компиляции в IL, явно указываете что собираете с отключенной отптимизацией
я в подобных случаях проверяю, что оптимизации выключены, не забыл и сейчас. Отключено, отключено - можете сами проверить. Вон же отладочные nop'ы для брейков в коде видны.
но и при этом пытаетесь сравнить "оптимизированность" кода.
Т.е. вы явно запретили компилятору оптимизировать, и делаете из последствий вашего запрета вывод, что компилятор иногда выдает "неоптимизированный код" :)
Если хотите получить оптимизированный код - включите оптимизацию. Если хотите получить код с отладочными вставками - выключите оптимизацию.
Вот этот артефакт:
IL_0007: stloc.0 IL_0008: br.s IL_000A IL_000A: ldloc.0
позволяет вам поставить брекпойнт на закрывающую скобку в
get { return _id; }
В случае нескольких return в методе - вы, скорее всего, получите пачку безусловных переходов на одну и ту же метку:
get { if (some) { return _id; } else { return 42; } } // брекпойнт на этой строке срабатывает всегда!
IL:
IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld bool ConsoleApp1.Abc::some IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: brfalse.s IL_0015 IL_000b: nop IL_000c: ldarg.0 IL_000d: ldfld int32 ConsoleApp1.Abc::_id IL_0012: stloc.1 IL_0013: br.s IL_001b IL_0015: nop IL_0016: ldc.i4.s 42 IL_0018: stloc.1 IL_0019: br.s IL_001b IL_001b: ldloc.1 IL_001c: ret
При отключенной отптимизации компилятор старается сделать код удобным для отладки, и, кроме nop-ов, вставляет вот такие вот костыли с явным сохранением возвращаемого значения в локальную переменную.
Включите оптимизацию - и отладочные хаки из IL исчезнут.
enSO:
Why is the 'br.s' IL opcode used in this case? Why does this very simple C# method produce such illogical CIL code?

Комментариев нет:

Отправить комментарий