如果我们使用内存栅栏来强制一致性,那么 "thread-thrashing" 是如何发生的呢?
If we use memory fences to enforce consistency, how does "thread-thrashing" ever occur?
在我知道 CPU 的存储缓冲区之前,我认为线程抖动只是在两个线程想要写入同一缓存行时发生。一个会阻止另一个写作。然而,这看起来相当同步。后来我才知道有一个store buffer,临时刷新写入。它被迫通过 SFENCE 指令刷新,有点暗示没有同步防止多个内核访问同一缓存行....
如果我们必须小心并使用 SFENCE,我完全不知道线程抖动是如何发生的?线程抖动意味着阻塞,而 SFENCE 意味着写入是异步完成的,程序员必须手动刷新写入??
(我对 SFENCE 的理解可能也很混乱——因为我还读到英特尔内存模型是 "strong",因此只有字符串 x86 指令才需要内存栅栏)。
有人可以消除我的困惑吗?
"Thrashing" 意味着多个内核检索相同的 cpu 缓存行,这会导致其他内核竞争同一缓存行的延迟开销。
所以,至少在我的词汇中,线程抖动发生在你有这样的事情时:
// global variable
int x;
// Thread 1
void thread1_code()
{
while(!done)
x++;
}
// Thread 2
void thread2_code()
{
while(!done)
x++;
}
(这段代码当然完全是胡说八道——我让它变得简单得可笑但毫无意义,因为没有复杂的代码很难解释线程本身发生了什么)
为简单起见,我们假设线程 1 始终 运行 在处理器 1 上,线程 2 始终 运行 在处理器 2 [1]
上
如果您 运行 SMP 系统上的这两个线程 - 我们刚刚启动了这段代码 [两个线程神奇地几乎同时启动,这与在真实系统中不同,相隔数千个时钟周期],线程一将读取 x
的值,更新它,然后将其写回。此时,线程2也是运行ning,它也会读取x
的值,更新它,然后写回。为此,它实际上需要询问其他处理器 "do you have (new value for) x
in your cache, if so, can you please give me a copy"。当然,处理器 1 将有一个新值,因为它刚刚存储回 x
的值。现在,该缓存行是 "shared"(我们的两个线程都有该值的副本)。线程二更新值并将其写回内存。当它这样做时,从该处理器发送另一个信号 "If anyone is holding a value of x
, please get rid of it, because I've just updated the value"。
当然,完全有可能两个线程都读取相同的 x
值,更新为相同的新值,并将其作为相同的新修改值写回。迟早一个处理器会写回一个低于另一个处理器写入的值的值,因为它落后了一点...
栅栏操作将有助于确保写入内存的数据在下一个操作发生之前实际上已经到达缓存,因为正如您所说,在内存更新实际到达内存之前有写缓冲区来保存内存更新.如果你没有 fence 指令,你的处理器可能会严重失相,并且在另一个有时间说 "do you have a new value for x
?" 之前多次更新值 - 然而,它并不能真正帮助防止处理器 1 从处理器 2 请求数据,处理器 2 立即请求数据 "back",因此系统可以尽可能快地来回乒乓缓存内容。
为确保只有一个处理器更新某些共享值,您需要使用所谓的原子指令。这些特殊指令旨在与写入缓冲区和高速缓存一起运行,这样它们就可以确保只有一个处理器实际拥有正在更新的高速缓存行的最新值,并且没有其他处理器能够更新该处理器完成更新之前的值。所以你永远不会得到 "read the same value of x
and write back the same value of x
" 或任何类似的东西。
由于缓存不适用于单个字节或单个整数大小的东西,您也可以 "false sharing"。例如:
int x, y;
void thread1_code()
{
while(!done) x++;
}
void thread2_code()
{
while(!done) y++;
}
现在,x
和 y
实际上不是同一个变量,但它们(很有可能,但我们不能 100% 确定)位于同一个缓存行中16、32、64 或 128 字节(取决于处理器架构)。因此,尽管 x
和 y
是不同的,但是当一个处理器说 "I've just updated x
, please get rid of any copies" 时,另一个处理器将同时摆脱它的(仍然正确的)值 y
摆脱 x
。我有这样一个例子,其中一些代码正在做:
struct {
int x[num_threads];
... lots more stuff in the same way
} global_var;
void thread_code()
{
...
global_var.x[my_thread_number]++;
...
}
当然,两个线程会紧挨着彼此更新值,性能很垃圾(比我们修复它时慢 6 倍:
struct
{
int x;
... more stuff here ...
} global_var[num_threads];
void thread_code()
{
...
global_var[my_thread_number].x++;
...
}
编辑澄清:
fence
不(正如我最近的编辑所解释的那样)"help" 反对在线程之间 ping-poning 缓存内容。它本身也不会阻止处理器之间不同步地更新数据 - 但是,它确实确保执行 fence
操作的处理器不会继续执行其他内存操作,直到此特定操作内存内容已获得 "out of" 处理器核心本身。由于存在不同的流水线阶段,而且大多数现代 CPU 都有多个执行单元,一个单元很可能是另一个单元的 "ahead",在执行流中技术上是 "behind"。栅栏将确保 "everything has been done here"。这有点像一级方程式赛车中那个有大挡板的人,它确保车手在所有新轮胎都安全地安装在车上之前不会从轮胎更换处开走(如果每个人都做了他们应该做的)。
MESI 或 MOESI 协议是一种状态机系统,可确保正确完成不同处理器之间的操作。一个处理器可以有一个修改值(在这种情况下,一个信号被发送到所有其他处理器到"stop using the old value"),一个处理器可以"own"这个值(它是这个数据的持有者,并且可以修改值),一个处理器可能有 "exclusive" 值(它是该值的唯一持有者,其他人都已经摆脱了他们的副本),它可能是 "shared" (不止一个处理器有一个副本,但是此处理器不应更新该值 - 它不是数据的 "owner")或无效(缓存中不存在数据)。 MESI 没有 "owned" 模式,这意味着侦听总线上的流量稍多("snoop" 表示 "Do you have a copy of x
"、"please get rid of your copy of x
" 等)
[1] 是的,处理器编号通常从零开始,但在我编写此附加段落时,我懒得回去将 thread1 重命名为 thread0,将 thread2 重命名为 thread1。
在我知道 CPU 的存储缓冲区之前,我认为线程抖动只是在两个线程想要写入同一缓存行时发生。一个会阻止另一个写作。然而,这看起来相当同步。后来我才知道有一个store buffer,临时刷新写入。它被迫通过 SFENCE 指令刷新,有点暗示没有同步防止多个内核访问同一缓存行....
如果我们必须小心并使用 SFENCE,我完全不知道线程抖动是如何发生的?线程抖动意味着阻塞,而 SFENCE 意味着写入是异步完成的,程序员必须手动刷新写入??
(我对 SFENCE 的理解可能也很混乱——因为我还读到英特尔内存模型是 "strong",因此只有字符串 x86 指令才需要内存栅栏)。
有人可以消除我的困惑吗?
"Thrashing" 意味着多个内核检索相同的 cpu 缓存行,这会导致其他内核竞争同一缓存行的延迟开销。
所以,至少在我的词汇中,线程抖动发生在你有这样的事情时:
// global variable
int x;
// Thread 1
void thread1_code()
{
while(!done)
x++;
}
// Thread 2
void thread2_code()
{
while(!done)
x++;
}
(这段代码当然完全是胡说八道——我让它变得简单得可笑但毫无意义,因为没有复杂的代码很难解释线程本身发生了什么)
为简单起见,我们假设线程 1 始终 运行 在处理器 1 上,线程 2 始终 运行 在处理器 2 [1]
上如果您 运行 SMP 系统上的这两个线程 - 我们刚刚启动了这段代码 [两个线程神奇地几乎同时启动,这与在真实系统中不同,相隔数千个时钟周期],线程一将读取 x
的值,更新它,然后将其写回。此时,线程2也是运行ning,它也会读取x
的值,更新它,然后写回。为此,它实际上需要询问其他处理器 "do you have (new value for) x
in your cache, if so, can you please give me a copy"。当然,处理器 1 将有一个新值,因为它刚刚存储回 x
的值。现在,该缓存行是 "shared"(我们的两个线程都有该值的副本)。线程二更新值并将其写回内存。当它这样做时,从该处理器发送另一个信号 "If anyone is holding a value of x
, please get rid of it, because I've just updated the value"。
当然,完全有可能两个线程都读取相同的 x
值,更新为相同的新值,并将其作为相同的新修改值写回。迟早一个处理器会写回一个低于另一个处理器写入的值的值,因为它落后了一点...
栅栏操作将有助于确保写入内存的数据在下一个操作发生之前实际上已经到达缓存,因为正如您所说,在内存更新实际到达内存之前有写缓冲区来保存内存更新.如果你没有 fence 指令,你的处理器可能会严重失相,并且在另一个有时间说 "do you have a new value for x
?" 之前多次更新值 - 然而,它并不能真正帮助防止处理器 1 从处理器 2 请求数据,处理器 2 立即请求数据 "back",因此系统可以尽可能快地来回乒乓缓存内容。
为确保只有一个处理器更新某些共享值,您需要使用所谓的原子指令。这些特殊指令旨在与写入缓冲区和高速缓存一起运行,这样它们就可以确保只有一个处理器实际拥有正在更新的高速缓存行的最新值,并且没有其他处理器能够更新该处理器完成更新之前的值。所以你永远不会得到 "read the same value of x
and write back the same value of x
" 或任何类似的东西。
由于缓存不适用于单个字节或单个整数大小的东西,您也可以 "false sharing"。例如:
int x, y;
void thread1_code()
{
while(!done) x++;
}
void thread2_code()
{
while(!done) y++;
}
现在,x
和 y
实际上不是同一个变量,但它们(很有可能,但我们不能 100% 确定)位于同一个缓存行中16、32、64 或 128 字节(取决于处理器架构)。因此,尽管 x
和 y
是不同的,但是当一个处理器说 "I've just updated x
, please get rid of any copies" 时,另一个处理器将同时摆脱它的(仍然正确的)值 y
摆脱 x
。我有这样一个例子,其中一些代码正在做:
struct {
int x[num_threads];
... lots more stuff in the same way
} global_var;
void thread_code()
{
...
global_var.x[my_thread_number]++;
...
}
当然,两个线程会紧挨着彼此更新值,性能很垃圾(比我们修复它时慢 6 倍:
struct
{
int x;
... more stuff here ...
} global_var[num_threads];
void thread_code()
{
...
global_var[my_thread_number].x++;
...
}
编辑澄清:
fence
不(正如我最近的编辑所解释的那样)"help" 反对在线程之间 ping-poning 缓存内容。它本身也不会阻止处理器之间不同步地更新数据 - 但是,它确实确保执行 fence
操作的处理器不会继续执行其他内存操作,直到此特定操作内存内容已获得 "out of" 处理器核心本身。由于存在不同的流水线阶段,而且大多数现代 CPU 都有多个执行单元,一个单元很可能是另一个单元的 "ahead",在执行流中技术上是 "behind"。栅栏将确保 "everything has been done here"。这有点像一级方程式赛车中那个有大挡板的人,它确保车手在所有新轮胎都安全地安装在车上之前不会从轮胎更换处开走(如果每个人都做了他们应该做的)。
MESI 或 MOESI 协议是一种状态机系统,可确保正确完成不同处理器之间的操作。一个处理器可以有一个修改值(在这种情况下,一个信号被发送到所有其他处理器到"stop using the old value"),一个处理器可以"own"这个值(它是这个数据的持有者,并且可以修改值),一个处理器可能有 "exclusive" 值(它是该值的唯一持有者,其他人都已经摆脱了他们的副本),它可能是 "shared" (不止一个处理器有一个副本,但是此处理器不应更新该值 - 它不是数据的 "owner")或无效(缓存中不存在数据)。 MESI 没有 "owned" 模式,这意味着侦听总线上的流量稍多("snoop" 表示 "Do you have a copy of x
"、"please get rid of your copy of x
" 等)
[1] 是的,处理器编号通常从零开始,但在我编写此附加段落时,我懒得回去将 thread1 重命名为 thread0,将 thread2 重命名为 thread1。