Почему существуют локальные переменные в байт-коде IL на основе стека

В промежуточном языке на основе стека, таком как байт-код CIL или Java, зачем нужны локальные переменные? Можно было бы просто использовать только стек. Это может быть не так просто для созданного вручную IL, но компилятор, безусловно, может это сделать. Но мой компилятор С# этого не делает.

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

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

С другой стороны, компилятор C# генерирует загрузки и сохранения для локальных переменных даже при компиляции с включенными оптимизациями. Почему?


Возьмем, к примеру, следующий надуманный пример кода:

static int X()
{
    int a = 3;
    int b = 5;
    int c = a + b;
    int d;
    if (c > 5)
        d = 13;
    else
        d = 14;
    c += d;
    return c;
}

При компиляции на C# с оптимизацией получается:

    ldc.i4.3        # Load constant int 3
    stloc.0         # Store in local var 0
    ldc.i4.5        # Load constant int 5
    stloc.1         # Store in local var 1
    ldloc.0         # Load from local var 0
    ldloc.1         # Load from local var 1
    add             # Add
    stloc.2         # Store in local var 2
    ldloc.2         # Load from local var 2
    ldc.i4.5        # Load constant int 5
    ble.s label1    # If less than, goto label1
    ldc.i4.s 13     # Load constant int 13
    stloc.3         # Store in local var 3
    br.s label2     # Goto label2
label1:
    ldc.i4.s 14     # Load constant int 14
    stloc.3         # Store in local var 3
label2:
    ldloc.2         # Load from local var 2
    ldloc.3         # Load from local var 3
    add             # Add
    stloc.2         # Store in local var 2
    ldloc.2         # Load from local var 2
    ret             # Return the value

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

    ldc.i4.3        # Load constant int 3
    ldc.i4.5        # Load constant int 5
    add             # Add
    dup             # Duplicate top stack element
    ldc.i4.5        # Load constant int 5
    ble.s label1    # If less than, goto label1
    ldc.i4.s 13     # Load constant int 13
    br.s label2     # Goto label2
label1:
    ldc.i4.s 14     # Load constant int 14
label2:
    add             # Add
    ret             # Return the value

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


person Daniel A.A. Pelsmaeker    schedule 16.09.2012    source источник
comment
Вы не можете всегда сделать такое простое преобразование, как вы продемонстрировали в своем примере.   -  person Greg Hewgill    schedule 17.09.2012
comment
Этот вопрос спрашивает, почему именованные слоты необходимы или почему оптимизированный вывод C# выглядит слишком многословным (например, используются в этом случае)?   -  person    schedule 17.09.2012
comment
Даже если именованные слоты необходимы или полезны в некоторых случаях (в каких случаях?), почему оптимизирующий компилятор не устраняет большинство операций загрузки и сохранения? Это кажется таким тривиальным. Я должен что-то упустить.   -  person Daniel A.A. Pelsmaeker    schedule 17.09.2012
comment
@Virtlink Ах, но это похоже на другой вопрос, чем вопрос, представленный в заголовке, и вступает в игру позже в посте :-) Я бы предложил сосредоточиться на двух отдельных но связанные вопросы (тот, что в заголовке, который является более гипотетическим), а затем тот, который фокусируется на том, почему (конкретный) C # компилируется именно так. Интересно, какой IL на самом деле приведет к более эффективному выполнению во время выполнения.   -  person    schedule 17.09.2012


Ответы (2)


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

В конце концов, несмотря на то, что в этом случае можно выразить все как чисто основанное на стеке, это может значительно усложнить код, раздуть его и затруднить оптимизацию (локальные переменные — идеальные кандидаты на роль кэшируется в регистрах).

Также помните, что в .NET вы можете передавать параметры по ссылке, как вы могли создать IL для вызова этого метода без локальной переменной?

bool TryGet(int key, out string value) {}
person Lucero    schedule 16.09.2012
comment
Я не понимаю, как эта подпись что-то меняет в самом вопросе. Может быть операция writeToOutParam (побочный эффект) для IL, которая в противном случае была бы полностью основана на стеке. - person ; 17.09.2012
comment
Я думаю, что Лусеро имеет в виду сторону звонящего. Когда локальных переменных нет, какой адрес локальной переменной передается методу TryGet в качестве второго аргумента? Но это, конечно, может быть «указатель» на слот стека. И разве значения в стеке не являются лучшими кандидатами для кэширования в регистрах? - person Daniel A.A. Pelsmaeker; 17.09.2012

Этот ответ чисто умозрительный, но я подозреваю, что ответ состоит из 3 частей.

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

2: Вы не можете игнорировать побочные эффекты. В примере, где все является просто литералом, очень легко понять, что значения находятся в стеке или в локальных переменных и, следовательно, полностью контролируются текущими инструкциями. Как только эти значения поступают из кучи, статической памяти или вызовов методов, вы больше не можете перетасовывать вещи, чтобы использовать Dup вместо локальных. Изменение порядка может изменить то, как все работает на самом деле, и вызвать непредвиденные последствия из-за побочных эффектов или внешнего доступа к общей памяти. Это означает, что обычно вы не можете выполнять эти оптимизации.

3: Предположение о том, что значения стека быстрее, чем локальные переменные, не является хорошим предположением — это может быть верно для конкретного преобразования IL-> машинного кода, что значения стека быстрее, но нет причин, по которым умный JIT не поместил бы расположение стека в память и локальную переменную в регистр. Задача JIT — узнать, что быстро, а что медленно для текущей машины, и задача JIT — решить проблему. По замыслу компилятор CIL не имеет ответа на вопрос, что быстрее — локальные или стековые; и поэтому измеримая разница между этими результатами заключается только в размере кода.

В совокупности 1 означает, что это сложно и имеет нетривиальную стоимость, 2 означает, что реальных случаев, когда это было бы полезно, немного, а 3 означает, что 1 и 2 в любом случае не имеют значения.

Даже если целью является минимизация размера CIL, что ЯВЛЯЕТСЯ измеримой целью компилятора CIL, причина № 2 описывает это как небольшое улучшение в небольшом количестве случаев. Принцип Парето не может сказать нам, что реализация такого рода оптимизации — ПЛОХАЯ идея, но он порекомендует, что есть, вероятно, ЛУЧШЕЕ использование времени разработчика.

person danwyand    schedule 17.12.2013
comment
и кое-что, что я забыл - в настоящем JIT, где скорость компиляции имеет значение, избегание операций Dup может привести к меньшему максимальному размеру стека, что может облегчить жизнь JIT. При равных результатах лучше достичь результата быстрее, чем медленнее. - person danwyand; 18.12.2013
comment
Но я все еще вижу, как компилятор вставляет совершенно ненужные локальные переменные (например, ldfld x; stloc.1; ldarg.0; ldloc.1; ldc.i4.1; add; stfld x; — для чего служит loc.1?), когда они ничего не добавляют. Или использование ldarg.0; ldarg.0; вместо ldarg.0; dup; будет безопасным во всех ситуациях? - person NetMage; 23.01.2018