Атомное приращение 64-битной переменной в 32-битной среде

Написав ответ на еще один вопрос, появилось несколько интересных вещей, и теперь я не понимаю, как Interlocked.Increment(ref long value) работает на 32-битных системах. Позвольте мне объяснить.

Нативный InterlockedIncrement64 теперь недоступен при компиляции для 32-разрядной среды, это имеет смысл, потому что в .NET вы не можете выровнять память по мере необходимости, и ее можно вызвать из управляемой, а затем она ее удалила.

В .NET мы можем вызвать Interlocked.Increment() со ссылкой на 64-битную переменную, у нас все еще нет ограничений относительно ее выравнивания (например, в структуре, где мы можем использовать FieldOffset и StructLayout), но в документации не упоминается никаких ограничений (AFAIK). Это волшебство, это работает!

Hans Passant отметил, что Interlocked.Increment() - это специальный метод, распознаваемый JIT-компилятором, и он выдает вызов COMInterlocked:: ExchangeAdd64() который затем вызывается FastInterlockExchangeAddLong, который является макросом для InterlockedExchangeAdd64, который разделяет те же ограничения InterlockedIncrement64.

Теперь я озадачен.

Забудьте в течение одной секунды управляемой среды и вернитесь к native. Почему InterlockedIncrement64 не может работать, но InterlockedExchangeAdd64 делает? InterlockedIncrement64 - макрос, если внутренние свойства недоступны и InterlockedExchangeAdd64 работает, то он может быть реализован как вызов InterlockedExchangeAdd64...

Вернемся к управляемому: как выполняется 64-разрядное увеличение атома на 32-битных системах? Я полагаю, что предложение "Эта функция является атомарной по отношению к вызовам других взаимосвязанных функций", но все же я не видел никакого кода (спасибо Хансу указать на более глубокую реализацию), чтобы сделать это. Выберем InterlockedExchangedAdd64 реализацию из WinBase.h, когда внутренние функции недоступны:

FORCEINLINE
LONGLONG
InterlockedExchangeAdd64(
    _Inout_ LONGLONG volatile *Addend,
    _In_    LONGLONG Value
    )
{
    LONGLONG Old;

    do {
        Old = *Addend;
    } while (InterlockedCompareExchange64(Addend,
                                          Old + Value,
                                          Old) != Old);

    return Old;
}

Как он может быть атомарным для чтения/записи?

Ответ 1

Вы должны следить за трейлом, InterlockedExchangeAdd64() переносит вас в заголовочный файл WinNt.h SDK. Где вы увидите много версий, в зависимости от целевой архитектуры.

Это обычно сворачивается в:

#define InterlockedExchangeAdd64 _InterlockedExchangeAdd64

который передает buck в собственный компилятор, объявленный в vc/include/intrin.h и реализованный контентом компилятора.

Или, другими словами, разные сборки CLR будут иметь разные реализации. На протяжении многих лет многие из них, x86, x64, Itanium, ARM, ARM8, PowerPC у меня на голове, я, конечно же, пропускаю некоторые, которые использовались для загрузки WindowsCE, прежде чем Apple сделало это неуместным. Для x86 это в конечном счете заботится LOCK CMPXCHNG8B, специальная инструкция процессора, которая может обрабатывать смещенные 64-битные переменные. У меня нет оборудования, чтобы посмотреть, как он выглядит на других 32-разрядных процессорах.

Имейте в виду, что целевая архитектура для управляемого кода не прибита во время компиляции. Это дрожание, которое адаптирует MSIL к цели во время выполнения. Это не так актуально для проектов С++/CLI, так как вам обычно нужно выбрать цель, если вы компилируете с /clr вместо/clr: pure и только x86 и x64 могут работать. Но сантехника на месте так или иначе, поэтому макрос просто не очень полезен.