dsi*_*cha 36 performance multithreading d thread-local-storage
我正在为D编程语言开发一个自定义标记释放样式的内存分配器,它通过从线程局部区域分配来工作.似乎线程本地存储瓶颈导致从这些区域分配内存的巨大(~50%)减速与相同的单线程版本的代码相比,即使在设计我的代码以使每个分配只有一个TLS查找/释放.这是基于在循环中多次分配/释放内存,我试图弄清楚它是否是我的基准测试方法的工件.我的理解是线程本地存储基本上只需要通过额外的间接层访问某些东西,类似于通过指针访问变量.这是不正确的?线程本地存储通常有多少开销?
注意:虽然我提到D,但我也对D不具体的一般答案感兴趣,因为如果它比最佳实现慢,D的线程局部存储的实现可能会有所改进.
Nil*_*nck 34
速度取决于TLS实现.
是的,你是正确的,TLS可以像指针查找一样快.在具有内存管理单元的系统上甚至可以更快.
对于指针查找,您需要来自调度程序的帮助.调度程序必须 - 在任务切换上 - 更新指向TLS数据的指针.
实现TLS的另一种快速方法是通过内存管理单元.这里TLS被视为与任何其他数据一样,但TLS变量在特殊段中分配.调度程序将在任务切换时将正确的内存块映射到任务的地址空间.
如果调度程序不支持任何这些方法,则编译器/库必须执行以下操作:
显然,为每个TLS数据访问执行所有这些操作需要一段时间,并且可能需要最多三个OS调用:获取ThreadId,获取并释放信号量.
信号量是btw所必需的,以确保没有线程从TLS指针列表读取而另一个线程正在产生新线程.(并因此分配新的TLS块并修改数据结构).
不幸的是,在实践中看到缓慢的TLS实现并不罕见.
And*_*riy 10
D中的线程本地人真的很快.这是我的测试.
64位Ubuntu,核心i5,dmd v2.052编译器选项:dmd -O -release -inline -m64
// this loop takes 0m0.630s
void main(){
int a; // register allocated
for( int i=1000*1000*1000; i>0; i-- ){
a+=9;
}
}
// this loop takes 0m1.875s
int a; // thread local in D, not static
void main(){
for( int i=1000*1000*1000; i>0; i-- ){
a+=9;
}
}
Run Code Online (Sandbox Code Playgroud)
因此,每1000*1000*1000线程本地访问,我们只丢失一个CPU内核的1.2秒.使用%fs寄存器访问线程本地 - 因此只涉及几个处理器命令:
用objdump -d拆解:
- this is local variable in %ecx register (loop counter in %eax):
8: 31 c9 xor %ecx,%ecx
a: b8 00 ca 9a 3b mov $0x3b9aca00,%eax
f: 83 c1 09 add $0x9,%ecx
12: ff c8 dec %eax
14: 85 c0 test %eax,%eax
16: 75 f7 jne f <_Dmain+0xf>
- this is thread local, %fs register is used for indirection, %edx is loop counter:
6: ba 00 ca 9a 3b mov $0x3b9aca00,%edx
b: 64 48 8b 04 25 00 00 mov %fs:0x0,%rax
12: 00 00
14: 48 8b 0d 00 00 00 00 mov 0x0(%rip),%rcx # 1b <_Dmain+0x1b>
1b: 83 04 08 09 addl $0x9,(%rax,%rcx,1)
1f: ff ca dec %edx
21: 85 d2 test %edx,%edx
23: 75 e6 jne b <_Dmain+0xb>
Run Code Online (Sandbox Code Playgroud)
也许编译器可能更聪明,并在循环到寄存器之前缓存线程本地并在最后将它返回到本地线程(与gdc编译器比较很有趣),但即使现在重要的是非常好的恕我直言.
在解释基准测试结果时需要非常小心.例如,D新闻组中最近的一个帖子从一个基准测试得出结论,dmd的代码生成导致了一个运算循环的主要减速,但实际上花费的时间主要是运行辅助函数做了很长的划分.编译器的代码生成与减速无关.
要查看为tls生成的代码类型,请编译和obj2asm此代码:
__thread int x;
int foo() { return x; }
Run Code Online (Sandbox Code Playgroud)
TLS在Windows上的实现方式与在Linux上的实现方式大不相同,在OSX上也会有很大不同.但是,在所有情况下,它将比简单加载静态内存位置更多的指令.相对于简单访问,TLS总是会变慢.在紧密循环中访问TLS全局变量也会很慢.尝试在临时缓存TLS值.
我在几年前编写了一些线程池分配代码,并将TLS句柄缓存到池中,效果很好.
我为嵌入式系统设计了多任务程序,从概念上讲,线程本地存储的关键要求是让上下文切换方法保存/恢复指向线程本地存储的指针以及 CPU 寄存器以及它正在保存/恢复的任何其他内容。对于一旦启动就将始终运行同一组代码的嵌入式系统,最简单的方法是保存/恢复一个指针,该指针指向每个线程的固定格式块。漂亮,干净,简单,高效。
如果不介意为每个线程中分配的每个线程局部变量(即使是那些从未实际使用它的变量)分配空间,并且如果所有将要在线程局部存储块中的东西都可以定义为单个结构。在这种情况下,访问线程局部变量几乎与访问其他变量一样快,唯一的区别是额外的指针取消引用。不幸的是,许多 PC 应用程序需要更复杂的东西。
在 PC 的某些框架上,如果使用这些变量的模块已在该线程上运行,则该线程只会为线程静态变量分配空间。虽然这有时可能是有利的,但这意味着不同的线程通常会有不同的本地存储布局。因此,线程可能需要对其变量所在的位置有某种可搜索的索引,并通过该索引将所有访问定向到这些变量。
我希望如果框架分配少量的固定格式存储,保留最后访问的 1-3 个线程局部变量的缓存可能会有所帮助,因为在许多情况下,即使单项缓存也可以提供命中率挺高的。