#c# #memory #compilation
#c# #память #Сборник
Вопрос:
Предположим, я использую MemoryMarshal.CreateSpan
для доступа к байтам локального типа значения, например, следующий (не очень полезный) код:
using System;
using System.Runtime.InteropServices;
// namespace and class boilerplate go here
private static void Main()
{
int value = 0;
Span<byte> valueBytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref value, 1));
var random = new Random();
while (value >= 0) // the check in question
{
random.NextBytes(valueBytes);
Console.WriteLine(value);
}
}
Хотя этот код работает по назначению, гарантированно ли указанная проверка сохранится при компиляции в IL и JIT-компиляции без оптимизации в true
, учитывая, что переменная value
не изменяется в цикле, кроме как косвенно через valueBytes
span? Могу ли я полагаться на чтение, value
дающее мне то, что записано при записи в valueBytes
, или это может быть уязвимо для изменения порядка? Или я просто параноик, потому что недавно немного увлекся C ?
(Обратите внимание, что я знаю, что существуют другие способы достижения желаемого эффекта от приведенного выше кода, это не вопрос о том, как получить полноразмерное 32-разрядное случайное целое число или проблема XY с каким-то большим приложением, в которое я пытаюсь поместить этот код, такого большого приложения не существует)
Комментарии:
1. Отличный вопрос! Я думаю, это также можно было бы расширить до того, как возможное изменение порядка команд может или не может повлиять на такой сценарий (например, изменение значения int через span, а затем обращение к значению int впоследствии. Гарантируется ли или нет, что компилятор C # или JITer обнаружит такую ситуацию и предотвратит изменение порядка доступа на запись в span после доступа к переменной value?)
2. @elgonzo — отличная мысль, я обновил вопрос, чтобы включить это.
3. Похоже, вас задело правило определения типов в C и C . Они ужасно злоупотребляли этим. C # документирует намерение исходного правила, приведение от меньшего к большему типу с большим требованием выравнивания не определено. Это просто жесткое поведение процессора, которое имело гораздо большее значение во времена RISC. Злоупотребление правилом, чтобы прекратить проверку псевдонимов указателей, по-настоящему злое поведение, это не проблема. Раздел 23.5.1 стандарта явно разрешает такое преобразование. Обратите внимание, что адресация на уровне байтов, как вы сделали здесь, также определена в C и C .
Ответ №1:
Я думаю, единственный определенный ответ могут дать люди, которые реализуют оптимизацию компилятора, как на Roslyn, так и на RyuJIT сторонах.
Поскольку вы используете .NET Core, вы, конечно, могли бы погрузиться в исходный код и найти ответ самостоятельно. Однако это будет ответом для конкретной версии компилятора.
Посмотрите на сгенерированный IL-код для вашего фрагмента:
// int value = 0;
ldc.i4.0
stloc.0
// MemoryMarshal.CreateSpan(ref value, 1)
ldloca.s 0
ldc.i4.1
call valuetype System.Span`1<!!0> System.Runtime.InteropServices.MemoryMarshal::CreateSpan<int32>(!!0amp;, int32)
// the rest is omitted
Обратите внимание, что ldloca.s
код операции. Эта операция загружает адрес локальной переменной в стек вычислений.
Хотя я не могу предоставить вам официальную ссылку, подтверждающую это, но я почти уверен, что компиляторы C # и JIT не будут оптимизировать эту локальную переменную только потому, что был использован ее адрес, поэтому есть вероятность, что эта локальная переменная будет изменена через свой адрес.
Если вы посмотрите на сгенерированный ассемблерный код, вы увидите именно это: локальная переменная есть и помещена в стек, это не переменная только для регистра.
// int value = 0;
xor ecx,ecx
mov dword ptr [rsp 3Ch],ecx
WHILE_LOOP_START:
// ... do stuff
// effectively: if (value >= 0) goto WHILE_LOOP_START
cmp dword ptr [rsp 3Ch],0
jge WHILE_LOOP_START
Попробуйте написать какой-нибудь код, который не выдает ldloca.s
код операции (например, просто value
в цикле), value
переменная, скорее всего, станет переменной только для регистра.
Если вы измените свой код таким образом, что value
он никогда не будет записан (за исключением инициализации), JIT-компилятор фактически полностью устранит проверку и саму переменную:
LOOP:
// Console.WriteLine(0)
xor ecx,ecx
call CONSOLE_WRITE_LINE
// while (true)
jmp LOOP
Интересно, однако, что компилятор C # не будет выполнять такую оптимизацию:
// int value = 0;
ldc.i4.0
stloc.0
br.s WHILE_CHECK
LOOP_START:
// Console.WriteLine(value)
ldloc.0
call void System.Console::WriteLine(int32)
WHILE_CHECK:
// effectively: if (value >= 0) goto LOOP_START
ldloc.0
ldc.i4.0
bge.s LOOP_START
Опять же, IL и ассемблерный код в моем ответе зависят от платформы и компилятора (даже от среды CLR). Я не могу предоставить вам подтверждающие документы. Но я почти уверен, что ни один компилятор не будет оптимизировать локальную переменную, адрес которой был получен и, более того, использовался в качестве аргумента при вызове методов / функций.
Возможно, кто-нибудь из команд Roslyn и RyuJIT мог бы дать вам лучший ответ.
Комментарии:
1. Спасибо за информацию, это довольно обнадеживает, что все недавние изменения, похоже, не заимствуют C footgun!
2. @dymanoid верен — поскольку локальный адрес «взят», jit-код должен быть достаточно консервативным и получать обновления. И jit не выполняет никакого устранения неоднозначности псевдонимов на основе типа.