Pull to refresh

Comments 31

Это всё, конечно, замечательно, но какая практическая польза от этой информации?
1. Более глубокое понимание того, что происходит. «Вы должны понимать как минимум на один уровень абстракции ниже того уровня, на котором вы кодируете» (с) Ли Кэмпбел.
2. Понимание таких вещей очень полезно, если вы занимаетесь кодогенерацией, делаете IL-инъекции, активно используете Emit и т.п. Ну и, разумеется, это пригодится тем, кто ввиду специфики своего проекта активно работает с IL.
3. А ещё есть такая штука, как критичный по производительности код. Если вы хотите очень сильно разогнать ваше C#-приложение, то вам очень не помешает понимать какие IL-инструкции стоят за каждой C#-строчкой (а лучше бы ещё и иметь представление про итоговые машинные коды).

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

Ну или просто для удовлетворения обыкновенного животного любопытства.
Это само собой =) Просто я отвечал на вопрос про практическую пользу.
Бьюсь об заклад, что при использовании emit вы не будете копировать это поведение, а тупо в лоб создадите массив (статический, если используется более одного раза).

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

То есть в целом ни для «сеньоров», ни для «джуниоров» эта информация пользы не представляет. Чисто любопытство удовлетворить. О чём я, собственно, и говорю.
До меня дошло. Эта информация ценна для джуниоров. Когда сеньор в следующий раз закричит, вытаращив глаза: «Ты что, опух при каждом вызове метода массив создавать?!» Джуниор сможет парировать: «А компилятор оптимизирует». :)
При таком способе инициализации массива дело, скорее всего, сводится к одному вызову memcpy (для первого массива). Отсюда и профит.
Вот мне интересно это, хотя я на C# не пишу. Вот в Java эта проблема не решена и объявление довольно длинного массива (хоть в статическом поле, хоть в методе) вызовет ошибку компиляции Code too large. И это не какой-то запредельный размер массива, что-то типа:

public class Test {
static final double[] vals = {
  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
... // 165 строчек пропущено
  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
  5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
};
}


Каждый элемент массива отнимает 8 байт байткода из 65535 допустимых (первые 127 элементов меньше), то есть влезает около 8000 элементов массива. Автогенерированная lookup-таблица для какого-нибудь численного алгоритма вполне может уткнуться в этот предел. Естественно, я должен знать это, когда работаю над алгоритмами. Хорошо, что в C# с этим проще. Разработчики на C# тоже должны это знать.
Я бы сказал, что полезно знать о наличии ограничений, а вот информация об их отсутствии менее полезна. Разве что в холиварах C# vs Java лишний аргумент. :)

Кстати, вы меня заинтриговали. Накопипастил кода на 25К строк, программа скомпилилась в 10-метровый исполняемый файл. Работает. :) За компанию проверил, что будет, если в огромном массиве один элемент сделать неконстантным — таки тоже оптимизировалось.

P.S. VS+R# под конец таки рухнули. Reflector тоже. Но скомпилировать и декомпилировать пару раз смогли.
На самом деле мне непонятно, почему нельзя было предусмотреть в class-файлах Java и в исполняемых файлах .Net специальную секцию типа «бинарный блок», в которую пихать в сыром виде содержимое массива, и специальную инструкцию байткода типа «скопируй кусок из бинарного блока в массив, который сейчас в верхушке стека». В шарпе что-то подобное сделали, но извратным путём с какими-то вложенными классами и вспомогательными методами. Может, конечно, это не настолько часто встречается, чтобы заморачиваться. Если данных много, разумно сделать дополнительный файл ресурсов в пакете и вычитать из него.
Э… Чтение из ресурсов — одна строчка кода (в шарпе, по крайней мере). Зачем какие-то новые абстракции?
По старой доброй традиции самое непонятное начинается после слова «очевидно»
-Сколько мы выводили эту формулу?
-Полтора месяца
-Пиши: «Легко показать, что...»
Ну шутки-шутками, может я, конечно, БЕЗНОГNМ, но вот идею фокуса с двумя переменными и как там вообще может образоваться выигрыш я не понял.
Меня это тоже удивило. Про выигрыш ничего не было сказано, да и его и не будет, т.к. простой вариант вместо dup будет делать stloc.0 и ldloc.0. Однако автор говорит про атомарность варианта с dup, но что он под этим подразумевает?
Я бы на вашем месте сделал акцент на том, что всё это особенности текущей версии компилятора C# от Microsoft. Насколько я знаю, спецификации C# нам ничего подобного не говорят. Пример: под Mono 2.4 приведённый код превращался в точности в
int[] ints = new int[3]; 
ints[0] = 1; 
ints[1] = 2; 
ints[2] = 3;

Исследование очень интересное и полезное, но не стоит забывать, что Microsoft нам не гарантирует, что в будущих версиях компилятора поведение не поменяется. Поэтому разумно будет указать версии компилятора C#, для которых выводы справедливы. Roslyn выдаёт аналогичные результаты, но кто его знает, что будет потом.

Барт де Смет писал оригинальный пост в далёком 2008, ещё до выхода Mono 2.0. В то время люди вообще не задумывались о том, что могут быть альтернативы реализации от Microsoft. А вот в наши дни надо бы указывать (хотя бы в примечаниях переводчика), что это особенности конкретных компиляторов.
Я думал о том, чтобы добавить строчку относительно mono, но возможное несоответствие поведения у mono и ms компиляторов должно быть уже привычной вещью. Насчёт указания версии компилятора — чёрт его знает, оригиналу статьи скоро вот уже 5 лет и пока ничего не изменилось.
p.s. поведение компилятора mono, используемого в unity3d, пока-что повторяет поведение, указанное в статье.
несоответствие поведения у mono и ms компиляторов должно быть уже привычной вещью

Ну, это не значит, что будет лишним напомнить об этом людям.

оригиналу статьи скоро вот уже 5 лет и пока ничего не изменилось

Сейчас .NET-отрасль развивается очень активно. Я не удивлюсь, если после выпуска Roslyn MS начнут улучшать IL-генерацию и чего-нибудь поменяют. В этом случае будущий читатель вашей статьи скажет спасибо за то, что вы указали версии компиляторов, для которых всё это работает.

поведение компилятора mono, используемого в unity3d, пока-что повторяет поведение, указанное в статье

Да, Xamarin пофиксили поведение в Mono 2.10. Но это не отменяет того факта, что есть ещё индивидуумы, которые сидят под Mono 2.4.
Еще было бы интересно посмотреть сравнение с точки зрения производительности, какой именно выигрыш дает эта оптимизация.
У меня есть в планах на будущее провести аккуратный бенчмарк. Как сделаю — скину ссылку на результаты.
> Вполне логично было-бы ожидать превращение этой конструкции в нечто подобное:

А мне вот кажется что нелогично. У нас конструктор и инициализацией значений. А если вспомнить во что превращается
var s = new SomeClass() { Id = 1 };
то как раз логично ожидать как раз вариант с промежуточной переменной.
Вынужден вас разочаровать: инициализатор обьекта компилируется самым наивным образом.
class c1
        {
            public int _1;
            public int _2;
            public int _3;
            public int _4;
        }

        void foo()
        {
            new c1() { _1 = 3, _3 = 2, _2 = 1 };
        }


.method private hidebysig instance void  foo() cil managed
{
  // Code size       31 (0x1f)
  .maxstack  2
  .locals init ([0] class ReflectionEmitArrayInitializerExample.Program/c1 '<>g__initLocal0')
  IL_0000:  nop
  IL_0001:  newobj     instance void ReflectionEmitArrayInitializerExample.Program/c1::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.3
  IL_0009:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_1
  IL_000e:  ldloc.0
  IL_000f:  ldc.i4.2
  IL_0010:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_3
  IL_0015:  ldloc.0
  IL_0016:  ldc.i4.1
  IL_0017:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_2
  IL_001c:  ldloc.0
  IL_001d:  pop
  IL_001e:  ret
} // end of method Program::foo
А какая версия Studio и .NET. Сейчас собрал такой же код под VS13+.NET4.5 — вижу 2 переменные (насколько я понимаю в IL)
    .method public hidebysig instance void Foo () cil managed 
    {
        .locals init (
            [0] class ClassLibrary1.Class1 c,
            [1] class ClassLibrary1.Class1 '<>g__initLocal0'
        )

        IL_0000: nop
        IL_0001: newobj instance void ClassLibrary1.Class1::.ctor()
        IL_0006: stloc.1
        IL_0007: ldloc.1
        IL_0008: ldc.i4.3
        IL_0009: stfld int32 ClassLibrary1.Class1::_1
        IL_000e: ldloc.1
        IL_000f: ldc.i4.1
        IL_0010: stfld int32 ClassLibrary1.Class1::_2
        IL_0015: ldloc.1
        IL_0016: ldc.i4.2
        IL_0017: stfld int32 ClassLibrary1.Class1::_3
        IL_001c: ldloc.1
        IL_001d: stloc.0
        IL_001e: ret
    }


Оригинал:
        public void Foo()
        {
            Class1 class1 = new Class1()
            {
                _1 = 3,
                _2 = 1,
                _3 = 2
            };
        }
Ну я о том же. В .NET 4.5 делается как раз через промежуточную переменную, что в принципе логично.
void foo()
        {
            new c1() { _1 = 3, _3 = 2, _2 = 1 };
        }

public void Foo()
        {
            Class1 class1 = new Class1()
            {
                _1 = 3,
                _2 = 1,
                _3 = 2
            };
        }
А, не заметил это с ходу. :) Ну тогда мое исходное утверждение получается верно — при использовании инициализаторов сборка класса происходит в промежуточной переменной. а уже потом присвоение в заданную. Соответственно и насчет инициализации массива тоже самое (см. мой первый комментарий). Не ясно тогда о чем спор :)
Sign up to leave a comment.

Articles