В чем смысл atomic.Load и atomic.Store - PullRequest
       49

В чем смысл atomic.Load и atomic.Store

1 голос
/ 27 октября 2019

В модели памяти Go ничего не говорится об атомистике и их связи с фехтованием памяти.

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

После недопонимания того, как это действительно работает, я обратился к источникам, в частности src / runtime / internal / atomic / atomic_amd64.go и обнаружил следующие реализации Load и Store:

//go:nosplit
//go:noinline
func Load(ptr *uint32) uint32 {
    return *ptr
}

Store реализован в asm_amd64.s в одном пакете.

TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12
    MOVQ    ptr+0(FP), BX
    MOVL    val+8(FP), AX
    XCHGL   AX, 0(BX)
    RET

Оба выглядят так, как если быони не имели ничего общего с параллелизмом.

Я изучил другие архитектуры, но реализация кажется эквивалентной.

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

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


func try() {
    var a, b int32

    go func() {
        // atomic.StoreInt32(&a, 1)
        // atomic.StoreInt32(&b, 1)
        a = 1
        b = 1
    }()

    for {
        // if n := atomic.LoadInt32(&b); n == 1 {
        if n := b; n == 1 {
            if a != 1 {
                panic("fail")
            }
            break
        }
        runtime.Gosched()
    }
}

func main() {
    n := 1000000000
    for i := 0; i < n ; i++ {
        try()
    }
}

Следующая мысль состояла в том, что компилятор совершает магиюпредоставить гарантии заказа. Ниже приведен список вариантов с атомарными Store и Load без комментариев. Полный список доступен на pastebin .

// Anonymous function implementation with atomic calls inlined

TEXT %22%22.try.func1(SB) gofile../path/atomic.go
        atomic.StoreInt32(&a, 1)
  0x816         b801000000      MOVL $0x1, AX
  0x81b         488b4c2408      MOVQ 0x8(SP), CX
  0x820         8701            XCHGL AX, 0(CX)
        atomic.StoreInt32(&b, 1)
  0x822         b801000000      MOVL $0x1, AX
  0x827         488b4c2410      MOVQ 0x10(SP), CX
  0x82c         8701            XCHGL AX, 0(CX)
    }()
  0x82e         c3          RET
// Important "cycle" part of try() function

 0x6ca          e800000000      CALL 0x6cf      [1:5]R_CALL:runtime.newproc
    for {
  0x6cf         eb12            JMP 0x6e3
        runtime.Gosched()
  0x6d1         90          NOPL
    checkTimeouts()
  0x6d2         90          NOPL
    mcall(gosched_m)
  0x6d3         488d0500000000      LEAQ 0(IP), AX      [3:7]R_PCREL:runtime.gosched_m·f
  0x6da         48890424        MOVQ AX, 0(SP)
  0x6de         e800000000      CALL 0x6e3      [1:5]R_CALL:runtime.mcall
        if n := atomic.LoadInt32(&b); n == 1 {
  0x6e3         488b442420      MOVQ 0x20(SP), AX
  0x6e8         8b08            MOVL 0(AX), CX
  0x6ea         83f901          CMPL $0x1, CX
  0x6ed         75e2            JNE 0x6d1
            if a != 1 {
  0x6ef         488b442428      MOVQ 0x28(SP), AX
  0x6f4         833801          CMPL $0x1, 0(AX)
  0x6f7         750a            JNE 0x703
  0x6f9         488b6c2430      MOVQ 0x30(SP), BP
  0x6fe         4883c438        ADDQ $0x38, SP
  0x702         c3          RET

Как видите, заборы или замки снова не установлены.

Примечание: все тестывыполняются на x86_64 и i5-8259U

Вопрос:

Итак, есть ли смысл переносить простой разыменование указателя в вызове функции или есть какой-то скрытый смысли почему эти атомы все еще работают как барьеры памяти? (если они делают)

1 Ответ

3 голосов
/ 27 октября 2019

Я вообще не знаю Go, но похоже, что x86-64 реализации .load() и .store() последовательно согласованы. Предположительно специально /по причине!

//go:noinline при загрузке означает, что компилятор не может переупорядочивать не-встроенную функцию черного ящика, я полагаю. На x86 это все, что вам нужно для загрузки со стороны последовательной согласованности или acq-rel. Простая загрузка x86 mov является загрузкой получения.

Сгенерированный компилятором код получает преимущества от строго упорядоченной модели памяти x86, которая представляет собой последовательную согласованность + буфер хранилища (с пересылкой хранилища)), т.е. acq / rel. Чтобы восстановить последовательную согласованность, вам нужно всего лишь очистить буфер хранилища после релиз-хранилища.

.store() записывается в asm, загружая свои аргументы стека и используя xchg в качестве хранилища seq-cst.


XCHG с памятью имеет неявный префикс lock, который является полным барьером;это эффективная альтернатива mov + mfence для реализации того, что C ++ назвал бы хранилищем memory_order_seq_cst.

Он сбрасывает буфер хранилища, прежде чем последующим загрузкам и хранилищам будет разрешено касаться L1d-кэша. Почему хранилище std :: atomic с последовательной последовательностью использует XCHG?

См.

...