Qtのソースにx86アセンブリがいくつかありました。
q_atomic_increment:
movl 4(%esp), %ecx
lock
incl (%ecx)
mov $0,%eax
setne %al
ret
.align 4,0x90
.type q_atomic_increment,@function
.size q_atomic_increment,.-q_atomic_increment
Googlingから、lock
命令によりCPUがバスをロックすることはわかっていましたが、CPUがいつバスを解放するかわかりませんか?
上記のコード全体について、このコードがAdd
を実装する方法がわかりません。
LOCK
は命令そのものではありません。これは、次の命令に適用される命令プレフィックスです。その命令は、メモリ上で読み取り-変更-書き込みを行うものでなければなりません(INC
、XCHG
、CMPXCHG
など)---この場合はincl (%ecx)
命令。
LOCK
プレフィックスは、CPUが操作中に適切なキャッシュラインの排他的所有権を持つことを保証し、特定の追加の順序保証を提供します。これはバスロックをアサートすることで実現できますが、CPUはこれを可能な限り回避します。バスがロックされている場合、ロックされた命令の期間のみです。
このコードは、スタックからインクリメントされる変数のアドレスをecx
レジスタにコピーし、lock incl (%ecx)
を実行してその変数をアトミックに1インクリメントします。次の2つの命令はeax
レジスタ(関数からの戻り値を保持する)は、変数の新しい値が0の場合は0に、それ以外の場合は1になります。操作はincrementであり、追加ではありません(名前の由来です)。
理解できない可能性があるのは、値をインクリメントするために必要なマイクロコードでは、最初に古い値を読み込む必要があるということです。
Lockキーワードは、実際に発生している複数のマイクロ命令をアトミックに動作するように強制します。
それぞれが同じ変数をインクリメントしようとする2つのスレッドがあり、両方が同じ元の値を同時に読み取った場合、両方が同じ値にインクリメントし、両方が同じ値を書き出します。
通常の予想である変数を2回インクリメントする代わりに、変数を1回インクリメントします。
Lockキーワードは、これが起こらないようにします。
Googleから、私はロック命令がCPUをバスにロックさせることを知っていましたが、CPUがバスをいつ解放するかわかりませんか?
LOCK
は命令プレフィックスであるため、次の命令にのみ適用されます。ソースはここでは明確にしませんが、実際の命令は_LOCK INC
_です。したがって、バスは増分のためにロックされ、その後ロック解除されます
上記のコード全体について、これらのコードがどのように追加を実装したのか理解できませんか?
それらは、Addを実装せず、増分を実装し、古い値が0の場合は戻り値を表示します。加算は_LOCK XADD
_を使用します(ただし、Windows InterlockedIncrement/Decrementも_LOCK XADD
_で実装されます。 )。
最小限の実行可能なC++スレッド+ LOCKインラインアセンブリの例
main.cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
unsigned long my_Arch_atomic_ulong = 0;
unsigned long my_Arch_non_atomic_ulong = 0;
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_Arch_non_atomic_ulong)
:
:
);
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_Arch_atomic_ulong)
:
:
);
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
assert(my_Arch_atomic_ulong == nthreads * niters);
std::cout << "my_Arch_non_atomic_ulong " << my_Arch_non_atomic_ulong << std::endl;
}
コンパイルして実行します:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread
./main.out 2 10000
可能な出力:
my_non_atomic_ulong 15264
my_Arch_non_atomic_ulong 15267
これから、LOCKプレフィックスが加算をアトミックにしたことがわかります。これがないと、多くの加算で競合状態が発生し、最後の合計カウントは同期20000未満になります。
Ubuntu 19.04 AMD64でテスト済み。