假设我有一个可能会或可能不会产生多个线程的应用程序.是否有必要保护需要与std :: mutex有条件同步的操作,如下所示,或者锁是如此便宜以至于单线程时无关紧要?
#include <atomic>
#include <mutex>
std::atomic<bool> more_than_one_thread_active{false};
void operation_requiring_synchronization() {
//...
}
void call_operation_requiring_synchronization() {
if (more_than_one_thread_active) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
operation_requiring_synchronization();
} else {
operation_requiring_synchronization();
}
}
Run Code Online (Sandbox Code Playgroud)
编辑
感谢所有回答和评论的人,非常有趣的讨论.
几点澄清:
应用程序处理输入块,并为每个块决定是以单线程还是并行或以其他方式并发方式处理它.不需要多线程是不可能的.
在operation_requiring_synchronization()通常由几个插入到全球标准集装箱.
当应用程序独立于平台并且应该在各种平台和编译器(过去,现在和将来)下运行良好时,分析当然是困难的.
根据目前为止的讨论,我倾向于认为优化是值得的.
我也认为std::atomic<bool> more_than_one_thread_active应该将其改为非原子的bool multithreading_has_been_initialized.最初的想法是当除了主要线程以外的所有线程都处于休眠状态时能够再次关闭标志,但我知道这可能是容易出错的.
将显式条件抽象为定制的lock_guard是一个好主意(并且有助于设计的未来变化,包括如果不认为优化值,则简单地恢复到std :: lock_guard).
我想知道的是lock xchg,mfence从一个线程访问内存位置的角度来看是否会有类似的行为,这个内存位置正在被其他线程进行变异(让我们随便说).它能保证我获得最新的价值吗?之后的内存读/写指令?
我混淆的原因是:
8.2.2"读取或写入不能通过I/O指令,锁定指令或序列化指令重新排序."
-Intel 64 Developers Manual Vol.3
这是否适用于线程?
mfence 状态:
对MFENCE指令之前发出的所有内存加载和存储到内存指令执行序列化操作.此序列化操作保证在MFENCE指令之前的任何加载或存储指令全局可见之前,在程序顺序之前的每条加载和存储指令都是全局可见的.MFENCE指令针对所有加载和存储指令,其他MFENCE指令,任何SFENCE和LFENCE指令以及任何序列化指令(例如CPUID指令)进行排序.
-Intel 64 Developers Manual Vol 3A
这听起来更有力.因为它听起来mfence几乎正在刷写写缓冲区,或者至少延伸到写缓冲区和其他内核以确保我未来的加载/存储是最新的.
当基准标记时,两个指令都需要约100个循环才能完成.所以我无论如何都看不出那么大的差异.
主要是我只是困惑.我的指令基于lock互斥体使用,但后来这些包含没有内存栅栏.然后,我看到锁免费使用内存栅栏编程,但没有锁.我知道AMD64有一个非常强大的内存模型,但过时的值可以在缓存中持续存在.如果lock行为与行为不同,mfence那么互斥量如何帮助您查看最新值?
x86 assembly multithreading cpu-architecture memory-barriers
我正在为C中的一些非常短的操作编写一些微基准测试代码.例如,我测量的一件事是根据传递的参数数量调用空函数需要多少个周期.
目前,我在每次操作之前和之后使用RDTSC指令进行计时,以获得CPU的循环计数.但是,我担心在第一个RDTSC之前发出的指令可能会减慢我正在测量的实际指令.我还担心在第二个RDTSC发布之前可能无法完成整个操作.
有没有人知道x86指令强制所有正在进行的指令在发出新指令之前提交?我被告知CPUID可能会这样做,但我一直无法找到任何说明的文档.
我正在研究使用x86 CPU中的时间戳寄存器(TSR)来测量基准性能.它是一个有用的寄存器,因为它以单调时间单位测量,不受时钟速度变化的影响.很酷.
这是一份英特尔文档,显示了使用TSR进行可靠基准测试的asm片段,包括使用cpuid进行管道同步.见第16页:
要读取开始时间,它说(我注释了一下):
__asm volatile (
"cpuid\n\t" // writes e[abcd]x
"rdtsc\n\t" // writes edx, eax
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t"
//
:"=r" (cycles_high), "=r" (cycles_low) // outputs
: // inputs
:"%rax", "%rbx", "%rcx", "%rdx"); // clobber
Run Code Online (Sandbox Code Playgroud)
我不知道为什么暂存寄存器用来取的价值观edx
和eax.为什么不删除MOVS和读取TSR值右出的edx
和eax?像这样:
__asm volatile(
"cpuid\n\t"
"rdtsc\n\t"
//
: "=d" (cycles_high), "=a" (cycles_low) // outputs
: // inputs
: "%rbx", "%rcx"); // clobber
Run Code Online (Sandbox Code Playgroud)
通过这样做,您可以保存两个寄存器,从而降低C编译器需要溢出的可能性.
我对吗?或者那些MOV在某种程度上是战略性的?
(我同意你确实需要临时寄存器来读取停止时间,因为在那种情况下指令的顺序是相反的:你有rdtscp,...,cpuid.cpuid指令破坏了rdtscp的结果).
谢谢
我一直试图谷歌我的问题,但老实说,我不知道如何简洁地陈述问题.
假设我在多核Intel系统中有两个线程.这些线程在同一个NUMA节点上运行.假设线程1写入X一次,然后只是偶尔读取它向前移动.进一步假设,线程2连续读取X. 如果我不使用内存栅栏,在线程1写入X和线程2看到更新值之间可以有多长时间?
我知道X的写入将转到存储缓冲区并从那里到缓存,此时MESIF将启动,线程2将通过QPI查看更新的值.(或者至少这是我收集到的).我假设存储缓冲区将被写入存储围栏中的缓存或者是否需要重用该存储缓冲区条目,但我不知道存储缓冲区是否已分配给写入.
最终我要为自己回答的问题是,如果线程2有可能在一个相当复杂的应用程序中看到线程1的写入几秒钟而正在做其他工作.
在英特尔优化手册似乎对存储缓冲区的数量存在于处理器的许多地方,但谈判没有谈存储缓冲区的大小.这是公共信息还是商店缓冲区的大小保留为微架构细节?
我正在研究的处理器主要是Broadwell和Skylake,但其他人的信息也不错.
另外,存储缓冲区究竟做了什么?
我已经看到这个答案和这个答案,但也显得清晰和明确有关的等价或不等价mfence和xchg没有非时间指示的假设下.
英特尔指令参考对于xchg提到这个指令是用于实现信号量或进程同步相似的数据结构有用,和其它参考文献的第8章卷3A.该参考文献陈述如下.
对于P6系列处理器,锁定操作会序列化所有未完成的加载和存储操作(即等待它们完成).对于奔腾4和英特尔至强处理器,此规则也是如此,但有一个例外.引用弱有序内存类型(例如WC内存类型)的加载操作可能无法序列化.
该mfence文件声称如下.
对MFENCE指令之前发出的所有内存加载和存储到内存指令执行序列化操作.此序列化操作保证在遵循MFENCE指令的任何加载或存储指令之前,按程序顺序在MFENCE指令之前的每个加载和存储指令都变为全局可见.1 MFENCE指令针对所有加载和存储指令,其他MFENCE指令,任何LFENCE和SFENCE指令以及任何序列化指令(例如CPUID指令)进行排序.MFENCE不会序列化指令流.
如果我们忽略弱有序的内存类型,xchg(暗示lock)是否包含了关于内存排序的所有mfence保证?
我编写了一个简单的“信封”类,以确保我正确理解C ++ 11原子语义。我有一个标头和一个有效负载,编写器清除该标头,填充有效负载,然后用递增的整数填充标头。这样的想法是,读取器然后可以读取标头,将有效负载换出,再次读取标头,如果标头相同,则读取器可以假定他们成功复制了有效负载。读者可能会错过一些更新是可以的,但是让他们获得更新的撕裂(其中来自不同更新的字节混合在一起)也不是可以的。永远只有一个读者和一个作家。
编写者使用释放内存顺序,而读者使用获取内存顺序。
是否存在通过原子存储/加载调用对memcpy重新排序的风险?还是可以将负载彼此重新排序?这永远不会让我流产,但也许我很幸运。
#include <iostream>
#include <atomic>
#include <thread>
#include <cstring>
struct envelope {
alignas(64) uint64_t writer_sequence_number = 1;
std::atomic<uint64_t> sequence_number;
char payload[5000];
void start_writing()
{
sequence_number.store(0, std::memory_order::memory_order_release);
}
void publish()
{
sequence_number.store(++writer_sequence_number, std::memory_order::memory_order_release);
}
bool try_copy(char* copy)
{
auto before = sequence_number.load(std::memory_order::memory_order_acquire);
if(!before) {
return false;
}
::memcpy(copy, payload, 5000);
auto after = sequence_number.load(std::memory_order::memory_order_acquire);
return before == after;
}
};
envelope g_envelope;
void reader_thread()
{
char local_copy[5000];
unsigned messages_received = 0;
while(true) {
if(g_envelope.try_copy(local_copy)) {
for(int i …Run Code Online (Sandbox Code Playgroud) 我知道如何使用LOCK线程安全地增加一个值:
lock inc [J];
Run Code Online (Sandbox Code Playgroud)
但是我如何以线程安全的方式阅读[J](或任何值)?LOCK前缀不能与mov一起使用.如果我做以下事情:
xor eax, eax;
lock add eax, [J];
mov [JC], eax;
Run Code Online (Sandbox Code Playgroud)
它在第2行引发错误.
通过使用内存标记为WB指令之间的主要区别是什么(回写)和WC(写入合并):什么是之间的不同MOVDQA和MOVNTDQA,什么是之间的不同VMOVDQA和VMOVNTDQ?
是不是对于内存标记为WC - 指令与[NT]通常没有区别(没有[NT]),并且内存标记为WB - 指令[NT]与它一起工作就好像它是一个内存WC?
assembly ×7
x86 ×7
intel ×3
benchmarking ×2
c ×2
c++ ×2
avx ×1
locking ×1
lockless ×1
memory-model ×1
performance ×1
rdtsc ×1
simd ×1
sse ×1
stdatomic ×1