Используя новейшие C # 7 , вы можете создавать методы расширения by-ref , чтобы избавиться от напряженной работы, связанной с постоянным хранением возврата. значение из вспомогательной функции обратно в переменную.
Это хорошо оптимизирует функции поворота и устраняет общий класс ошибок, когда вы забываете повторно сохранять возвращаемое значение функции, но в то же время, возможно, вводите новый, совершенно другой тип ошибок - где ValueTypes
непреднамеренно получают изменено in-situ , когда вы не хотели или ожидаете, что они будут.
public static void Rol(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);
public static void Rol(ref this ulong ul, int N) => ul = (ul << N) | (ul >> (64 - N));
public static void Ror(ref this ulong ul) => ul = (ul << 63) | (ul >> 1);
public static void Ror(ref this ulong ul, int N) => ul = (ul << (64 - N)) | (ul >> N);
/// note: ---^ ^---^--- extension method can now use 'ref' for ByRef semantics
Обычно я обязательно ставлю [MethodImpl(MethodImplOptions.AggressiveInlining)]
на небольшие методы, подобные этим, но после некоторого исследования (на x64) я обнаружил, что в этом нет необходимости. Если JIT определяет, что метод является приемлемым (например, если вы снимите флажок отладчика VisualStudio «Подавить оптимизацию JIT» , который включен по умолчанию), методы будут встроены независимо, и это имеет место в данном случае.
В случае, если термин незнаком, JIT или «точно в срок» относится к разовому преобразованию инструкций IL в собственный код, настроенный для обнаруженной платформы. во время выполнения процесс, который происходит по требованию, для каждого метода как .NET , программа запускает .
Чтобы продемонстрировать использование метода расширения by-ref , я сосредоточусь только на первом показанном выше методе «повернуть влево» и сравню вывод JIT между традиционный по значению метод расширения и более новый по-ref подход. Вот два метода тестирования, которые нужно сравнить на x64 Release в .NET 4.7 на Windows 10. Как отмечалось выше, это будет с оптимизацией JIT, а не -suppressed ', поэтому в этих условиях тестирования, как вы увидите, функции полностью исчезнут во встроенном коде.
static ulong Rol_ByVal(this ulong ul) => (ul << 1) | (ul >> 63);
static void Rol_ByRef(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);
// notice reassignment here ---^ (c̲a̲l̲l̲e̲e̲ doing it instead of caller)
А вот код C # для каждого соответствующего сайта вызова. Поскольку полностью оптимизированный для JIT код AMD64 настолько мал, я могу просто включить его и здесь. Это оптимальный случай:
static ulong x = 1; // static so it won't be optimized away in this simple test
// ------- ByVal extension method; c̲a̲l̲l̲e̲r̲ must reassign 'x' with the result -------
x = x.Rol_ByVal();
// 00007FF969CC0481 mov rax,qword ptr [7FF969BA4888h]
// 00007FF969CC0487 rol rax,1
// 00007FF969CC048A mov qword ptr [7FF969BA4888h],rax
// ------- New in C#7, ByRef extension method can directly alter 'x' in-situ -------
x.Rol_ByRef();
// 00007FF969CC0491 rol qword ptr [7FF969BA4888h],1
Wow. Да, это не шутка. Сразу видно, что явное отсутствие OpCodes.Rot
семейства инструкций на промежуточном языке ECMA CIL (.NET) в значительной степени не является проблемой; Джиттер мог видеть сквозь нашу кучу кода обхода C # (ul << 1) | (ul >> 63)
, чтобы предугадать его основное намерение, которое в обоих случаях реализует JIT x64, просто испуская собственную инструкцию rol
. Впечатляет, что версия ByRef использует одну инструкцию для выполнения вращения непосредственно по целевому адресу основной памяти, даже не загружая его в регистр.
В случае ByVal вы все еще можете видеть остаточный след избыточного копирования, который был необходим, чтобы оставить исходное значение вызывающего объекта неизменным до полной оптимизации вызываемого метода (как это является сущностью семантики типа-значения). ). Для поворота целого числа здесь это просто дополнительная выборка / сохранение целевого числа в 64-битный регистр rax
.
Чтобы прояснить это, давайте снова подавим оптимизацию JIT в сеансе отладки. Это заставит наши методы расширения помощника вернуться, с полными телами и кадрами стека, чтобы лучше объяснить первое предложение предыдущего абзаца. Сначала давайте посмотрим на сайты вызовов. Здесь мы видим эффект традиционной семантики ValueType
, которая сводится к тому, что ни один кадр нижнего стека не может манипулировать копиями ValueType
любого родительского кадра:
по значению:
x = x.Rol_ByVal();
// 00007FF969CE049C mov rcx,qword ptr [7FF969BC4888h]
// 00007FF969CE04A3 call 00007FF969CE00A8
// 00007FF969CE04A8 mov qword ptr [rbp-8],rax
// 00007FF969CE04AC mov rcx,qword ptr [rbp-8]
// 00007FF969CE04B0 mov qword ptr [7FF969BC4888h],rcx
* ** +1078 +1079 *
по ссылке * 1 081 ** ** 1083 тысяча восемьдесят-два *
x.Rol_ByRef();
// 00007FF969CE04B7 mov rcx,7FF969BC4888h
// 00007FF969CE04C1 call 00007FF969CE00B0
// ...all done, nothing to do here; the callee did everything in-place for us
Как и следовало ожидать от кода C # , связанного с каждым из этих двух фрагментов, мы видим, что у вызывающей стороны by-val есть куча работы делать после возврата звонка. Это процесс перезаписи родительской копии значения ulong
'x' полностью независимым значением ulong
, которое возвращается в регистр rax
.
Теперь давайте посмотрим на код для вызываемых целевых функций. Чтобы увидеть их, нужно заставить JIT «подавить» оптимизацию. Ниже приведен собственный код, который выпускает JIT Release x64 для функций Rol_ByVal
и Rol_ByRef
.
Чтобы сосредоточиться на крошечном, но принципиальном различии между ними, я отбросил некоторые административные шаблоны. (Я оставил настройку стека фрейма и разбор для контекста, и чтобы показать, как в этом примере этот вспомогательный материал в значительной степени затмевает фактические содержательные инструкции.) Можете ли вы увидеть косвенную работу ByRef в работе? Хорошо, помогает то, что я указал на это: - /
static ulong Rol_ByVal(this ulong ul) => (ul << 1) | (ul >> 63);
// 00007FF969CD0760 push rbp
// 00007FF969CD0761 sub rsp,20h
// 00007FF969CD0765 lea rbp,[rsp+20h]
// ...
// 00007FF969CE0E4C mov rax,qword ptr [rbp+10h]
// 00007FF969CE0E50 rol rax,1
// 00007FF969CD0798 lea rsp,[rbp]
// 00007FF969CD079C pop rbp
// 00007FF969CD079D ret
static void Rol_ByRef(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);
// 00007FF969CD0760 push rbp
// 00007FF969CD0761 sub rsp,20h
// 00007FF969CD0765 lea rbp,[rsp+20h]
// ...
// 00007FF969CE0E8C mov rax,qword ptr [rbp+10h]
// 00007FF969CE0E90 rol qword ptr [rax],1 <--- !
// 00007FF969CD0798 lea rsp,[rbp]
// 00007FF969CD079C pop rbp
// 00007FF969CD079D ret
Вы можете заметить, что оба вызова фактически передают экземпляр родительского значения ulong
по ссылке - оба примера идентичны в этом отношении. Родитель указывает адрес, где его личная копия ul
находится в верхнем фрейме стека. Оказывается, нет необходимости изолировать вызываемых абонентов от чтения тех случаев, когда они лежат, при условии, что мы можем быть уверены, что они никогда не пишут в эти указатели. Это «ленивый» или отложенный подход, который назначает каждому нижнему (дочернему) фрейму стека ответственность за сохранение семантики ValueType его вышестоящих вызывающих. Вызывающему не нужно предварительно копировать любые ValueType
, переданные в дочерний фрейм, если ребенок никогда не перезаписывает его; чтобы максимально избежать ненужного копирования, только ребенок может принять самое последнее решение.
Также интересно, что у нас могло бы быть объяснение здесь неуклюжего использования rax
в первом примере 'ByVal', который я показал. После того, как метод значения по значению был полностью уменьшен с помощью встраивания, почему вращение все еще должно происходить в регистре?
Что ж, в этих последних двух версиях с полным текстом метода ясно, что первый метод возвращает ulong
, а второй - void
. Поскольку возвращаемое значение передается в rax
, метод ByVal должен в любом случае извлекать его в этот регистр, так что вращать его там тоже нетрудно. Поскольку метод ByRef не должен возвращать никакого значения, ему не нужно ничего прикреплять к вызывающему объекту, не говоря уже о rax
. Кажется вероятным, что «отсутствие необходимости беспокоиться о rax
» освобождает код ByRef для нацеливания на экземпляр ulong
, к которому его родительский ресурс поделился «где он лежит», используя причудливый qword ptr
для косвенной передачи в память кадров стека родителя, вместо использования регистра. Так что это мое умозрительное, но, возможно, заслуживающее доверия объяснение "остаточной rax
" тайны, которую мы видели ранее.