web-dev-qa-db-de.com

Unerwartete x64-Assembly für __atomic_fetch_or mit gcc 7.3

Ich versuche, ein 64-Bit-Integral als Bitmap zu verwenden und den Besitz einzelner Bits atomar zu erwerben/freizugeben.

Zu diesem Zweck habe ich folgenden Lockless-Code geschrieben:

#include <cstdint>
#include <atomic>

static constexpr std::uint64_t NO_INDEX = ~std::uint64_t(0);

class AtomicBitMap {
public:
    static constexpr std::uint64_t occupied() noexcept {
        return ~std::uint64_t(0);
    }

    std::uint64_t acquire() noexcept {
        while (true) {
            auto map = mData.load(std::memory_order_relaxed);
            if (map == occupied()) {
                return NO_INDEX;
            }

            std::uint64_t index = __builtin_ctzl(~map);
            auto previous =
                mData.fetch_or(bit(index), std::memory_order_relaxed);
            if ((previous & bit(index)) == 0) {
                return index;
            }
        }
    }

private:
    static constexpr std::uint64_t bit(std::uint64_t index) noexcept {
        return std::uint64_t(1) << index;
    }

    std::atomic_uint64_t mData{ 0 };
};

int main() {
    AtomicBitMap map;
    return map.acquire();
}

Welche, auf Godbolt ergibt die folgende Versammlung in Isolation:

main:
  mov QWORD PTR [rsp-8], 0
  jmp .L3
.L10:
  not rax
  rep bsf rax, rax
  mov edx, eax
  mov eax, eax
  lock bts QWORD PTR [rsp-8], rax
  jnc .L9
.L3:
  mov rax, QWORD PTR [rsp-8]
  cmp rax, -1
  jne .L10
  ret
.L9:
  movsx rax, edx
  ret

Und genau das habe ich erwartet1.

@Jester hat es heroisch geschafft, mein 97-Zeilen-Wiedergabegerät auf ein viel einfacheres 44 Zeilen-Wiedergabegerät zu reduzieren, das ich weiter auf 35 Zeilen reduzierte: -

using u64 = unsigned long long;

struct Bucket {
    u64 mLeaves[16] = {};
};

struct BucketMap {
    u64 acquire() noexcept {
        while (true) {
            u64 map = mData;

            u64 index = (map & 1) ? 1 : 0;
            auto mask = u64(1) << index;

            auto previous =
                __atomic_fetch_or(&mData, mask, __ATOMIC_SEQ_CST);
            if ((previous & mask) == 0) {
                return index;
            }
        }
    }

    __attribute__((noinline)) Bucket acquireBucket() noexcept {
        acquire();
        return Bucket();
    }

    volatile u64 mData = 1;
};

int main() {
    BucketMap map;
    map.acquireBucket();
    return 0;
}

Was erzeugt die folgende Assembly:

BucketMap::acquireBucket():
  mov r8, rdi
  mov rdx, rsi

.L2:
  mov rax, QWORD PTR [rsi]
  xor eax, eax
  lock bts QWORD PTR [rdx], rax
  setc al
  jc .L2
  mov rdi, r8
  mov ecx, 16
  rep stosq
  mov rax, r8
  ret

main:
  sub rsp, 152
  lea rsi, [rsp+8]
  lea rdi, [rsp+16]
  mov QWORD PTR [rsp+8], 1
  call BucketMap::acquireBucket()
  xor eax, eax
  add rsp, 152
  ret

Der xor eax,eax bedeutet, dass die Assembly hier immer versucht, den Index 0 ... zu erhalten, was zu einer Endlosschleife führt.

Ich kann nur zwei Erklärungen für diese Versammlung sehen:

  1. Ich habe irgendwie undefiniertes Verhalten ausgelöst.
  2. In gcc gibt es einen Code-Generierungsfehler.

Und ich habe alle meine Ideen erschöpft, was UB auslösen könnte.

Kann jemand erklären, warum gcc diesen xor eax,eax generiert?

Hinweis: gcc vorläufig gemeldet als https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86314 .


Verwendete Compiler-Version:

$ gcc --version
gcc (GCC) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is 
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR 
PURPOSE.

Compiler-Flags:

-Wall -Wextra -Werror -Wduplicated-cond -Wnon-virtual-dtor -Wvla 
-rdynamic -Wno-deprecated-declarations -Wno-type-limits 
-Wno-unused-parameter -Wno-unused-local-typedefs -Wno-unused-value 
-Wno-aligned-new -Wno-implicit-fallthrough -Wno-deprecated 
-Wno-noexcept-type -Wno-register -ggdb -fno-strict-aliasing 
-std=c++17 -Wl,--no-undefined -Wno-sign-compare 
-g -O3 -mpopcnt

1 Eigentlich ist es besser als ich erwartet hatte. Der Compiler hat verstanden, dass fetch_or(bit(index)) gefolgt von previous & bit(index) das Äquivalent der Verwendung von bts und der Überprüfung des CF-Flags in reinem Gold ist.

14
Matthieu M.

Dies ist ein Fehler bei der Gucklochoptimierung in gcc, siehe # 86413 , der die Versionen 7.1, 7.2, 7.3 und 8.1 betrifft. Das Update ist bereits in Version 7.4 und 8.2 enthalten.


Die kurze Antwort ist, dass die bestimmte Codefolge (fetch_or + Prüfergebnis) eine setcc (set conditional, auch basierend auf dem Status der Flags) generiert, gefolgt von einer movzbl (Verschieben und Nullstellen); In 7.x wurde eine Optimierung eingeführt, die eine setcc, gefolgt von movzbl, in eine xor, gefolgt von setcc, umwandelt, jedoch fehlten einige Überprüfungen, die dazu führten, dass die xor möglicherweise ein noch benötigtes Register blockierte (in diesem Fall eax).


Die längere Antwort ist, dass fetch_or entweder als cmpxchg für die Allgemeinheit oder, wenn nur ein Bit gesetzt wird, als bts (Bittest und gesetzt) ​​implementiert werden kann. Als eine weitere in 7.x eingeführte Optimierung generiert gcc jetzt hier eine bts (gcc 6.4 generiert immer noch eine cmpxchg). bts setzt das Übertragsflag (CF) auf den vorherigen Wert des Bits.

Das heißt, auto previous = a.fetch_or(bit); auto n = previous & bit; erzeugt Folgendes:

  • lock bts QWORD PTR [<address of a>], <bit index>, um das Bit zu setzen und seinen vorherigen Wert zu erfassen,
  • setc <n>l, um die unteren 8 Bits von r<n>x auf den Wert des Übertragsflags (CF) zu setzen,
  • movzx e<n>x, <n>l, um die oberen Bits von r<n>x auf Null zu setzen.

Und dann wird die Gucklochoptimierung angewendet, die die Dinge durcheinander bringt.

gcc trunk erzeugt jetzt richtige Assembly :

BucketMap::acquireBucket():
    mov rdx, rdi
    mov rcx, rsi
.L2:
    mov rax, QWORD PTR [rsi]
    and eax, 1
    lock bts QWORD PTR [rcx], rax
    setc al
    movzx eax, al
    jc .L2
    mov rdi, rdx
    mov ecx, 16
    rep stosq
    mov rax, rdx
    ret
main:
    sub rsp, 152
    lea rsi, [rsp+8]
    lea rdi, [rsp+16]
    mov QWORD PTR [rsp+8], 1
    call BucketMap::acquireBucket()
    xor eax, eax
    add rsp, 152
    ret

Obwohl die Optimierung leider nicht mehr zutrifft, bleiben setc + mov anstelle von xor + setc... übrig, aber zumindest ist es richtig!

5
Matthieu M.

Als Randbemerkung können Sie das niedrigste 0-Bit mit einer direkten Bit-Manipulation finden:

template<class T>
T find_lowest_0_bit_mask(T value) {
    T t = value + 1;
    return (t ^ value) & t;
}

Gibt die Bitmaske anstelle des Bitindex zurück.

Vorbedingungen: T muss vorzeichenlos sein, value muss mindestens 1 Null-Bit enthalten.


mData.load muss mit mData.fetch_or synchronisieren, also sollte es sein

mData.load(std::memory_order_acquire)

und

mData.fetch_or(..., std::memory_order_release)

Und, IMO, es gibt etwas über diese Bit-Intrinsics, die dazu führen, dass es eine falsche Assembly mit clang generiert, siehe .LBB0_5-Schleife, die eindeutig falsch ist , weil sie ständig versucht, das gleiche Bit zu setzen, anstatt ein anderes Bit neu zu berechnen. Eine Version, die korrekte Assembly generiert :

#include <cstdint>
#include <atomic>

static constexpr int NO_INDEX = -1;

template<class T>
T find_lowest_0_bit_mask(T value) {
    T t = value + 1;
    return (t ^ value) & t;
}

class AtomicBitMap {
public:
    static constexpr std::uint64_t occupied() noexcept { return ~std::uint64_t(0); }

    int acquire() noexcept {
        auto map = mData.load(std::memory_order_acquire);
        while(map != occupied()) {
            std::uint64_t mask = find_lowest_0_bit_mask(map);
            if(mData.compare_exchange_weak(map, map | mask, std::memory_order_release))
                return __builtin_ffsl(mask) - 1;
        }
        return NO_INDEX;
    }

    void release(int i) noexcept {
        mData.fetch_and(~bit(i), std::memory_order_release);
    }

private:
    static constexpr std::uint64_t bit(int index) noexcept { 
        return std::uint64_t(1) << index; 
    }

    std::atomic_uint64_t mData{ 0 };
};
1

xor-zero/set flags/setcc ist normalerweise der beste Weg, um eine 32-Bit-0/1-Ganzzahl zu erstellen.

Offensichtlich ist dies nur sicher, wenn Sie ein Ersatzregister für xor- zero haben, ohne Eingaben für die Flag-Einstellanweisungen zu zerstören. Dies ist also eindeutig ein Fehler.

(Andernfalls können Sie setcc dl/movzx eax,dl verwenden. Separate Register sind vorzuziehen, so dass die movzx auf einigen CPUs null Latenzzeit (mov-eliminierung) sein kann, auf anderen CPUs jedoch auf dem kritischen Pfad. Daher ist das Xor/set-flags/setcc-Idiom vorzuziehen weniger Anweisungen befinden sich auf dem kritischen Pfad.)

IDK, warum gcc den ganzzahligen Wert von (previous & mask) == 0 in einem Register überhaupt erstellt; das ist wahrscheinlich ein Teil des Fehlers.

0
Peter Cordes