我正在为D编程语言开发一个自定义标记释放样式的内存分配器,它通过从线程局部区域分配来工作.似乎线程本地存储瓶颈导致从这些区域分配内存的巨大(~50%)减速与相同的单线程版本的代码相比,即使在设计我的代码以使每个分配只有一个TLS查找/释放.这是基于在循环中多次分配/释放内存,我试图弄清楚它是否是我的基准测试方法的工件.我的理解是线程本地存储基本上只需要通过额外的间接层访问某些东西,类似于通过指针访问变量.这是不正确的?线程本地存储通常有多少开销?
注意:虽然我提到D,但我也对D不具体的一般答案感兴趣,因为如果它比最佳实现慢,D的线程局部存储的实现可能会有所改进.
速度取决于TLS实现.
是的,你是正确的,TLS可以像指针查找一样快.在具有内存管理单元的系统上甚至可以更快.
对于指针查找,您需要来自调度程序的帮助.调度程序必须 - 在任务切换上 - 更新指向TLS数据的指针.
实现TLS的另一种快速方法是通过内存管理单元.这里TLS被视为与任何其他数据一样,但TLS变量在特殊段中分配.调度程序将在任务切换时将正确的内存块映射到任务的地址空间.
如果调度程序不支持任何这些方法,则编译器/库必须执行以下操作:
获取当前的ThreadId
拿一个信号量
通过ThreadId查找指向TLS块的指针(可以使用地图等)
释放信号量
返回指针.
显然,为每个TLS数据访问执行所有这些操作需要一段时间,并且可能需要最多三个OS调用:获取ThreadId,获取并释放信号量.
信号量是btw所必需的,以确保没有线程从TLS指针列表读取而另一个线程正在产生新线程.(并因此分配新的TLS块并修改数据结构).
不幸的是,在实践中看到缓慢的TLS实现并不罕见.
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; } }
因此,每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>
也许编译器可能更聪明,并在循环到寄存器之前缓存线程本地并在最后将它返回到本地线程(与gdc编译器比较很有趣),但即使现在重要的是非常好的恕我直言.
在解释基准测试结果时需要非常小心.例如,D新闻组中最近的一个帖子从一个基准测试得出结论,dmd的代码生成导致了一个运算循环的主要减速,但实际上花费的时间主要是运行辅助函数做了很长的划分.编译器的代码生成与减速无关.
要查看为tls生成的代码类型,请编译和obj2asm此代码:
__thread int x; int foo() { return x; }
TLS在Windows上的实现方式与在Linux上的实现方式大不相同,在OSX上也会有很大不同.但是,在所有情况下,它将比简单加载静态内存位置更多的指令.相对于简单访问,TLS总是会变慢.在紧密循环中访问TLS全局变量也会很慢.尝试在临时缓存TLS值.
我在几年前编写了一些线程池分配代码,并将TLS句柄缓存到池中,效果很好.