c++ 线程局部奇怪内联

aiazj4mn  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(149)

因此,对于下面的代码,我看到了一个奇怪的内联/优化人工制品。
我很好奇,在一些我不欣赏的假设场景中,它们是否是“必要的”?
Godbolt:https://godbolt.org/z/M8PW1obE7

  1. #include <cstdint>
  2. #include <stdio.h>
  3. struct ThreadStateLogger
  4. {
  5. static thread_local struct Instance
  6. {
  7. char TLS_byteLoc[3] {' ', 0, 0};
  8. uint8_t TLS_numBytes {1};
  9. // 4 wasted bytes here...
  10. char* TLS_byteLocPtr {TLS_byteLoc};
  11. void Log(char v)
  12. {
  13. TLS_byteLocPtr[0] = v;
  14. }
  15. void Log(char v1, char v2)
  16. {
  17. TLS_byteLocPtr[0] = v1;
  18. if (TLS_numBytes>1)
  19. TLS_byteLocPtr[1] = v2;
  20. }
  21. } instance;
  22. static void Log(char v1, char v2)
  23. {
  24. instance.Log(v1, v2);
  25. }
  26. // static void Log(char v1, char v2)
  27. // {
  28. // instance.TLS_byteLocPtr[0] = v1;
  29. // if (instance.TLS_numBytes>1)
  30. // instance.TLS_byteLocPtr[1] = v2;
  31. // }
  32. };
  33. extern ThreadStateLogger theThreadStateLogger;
  34. int main()
  35. {
  36. ThreadStateLogger::Log('a', 'b');
  37. // printf("Hello world");
  38. ThreadStateLogger::Log('c', 'd');
  39. return 0;
  40. }

整个主要实现都与-O3内联,这正是我想要的:-)
因此,第一个Log()调用似乎正确地检查了这个TLS是否需要分配,然后使用__tls_get_addr@PLT获得转换后的地址,这一切都很好。
第二个Log()调用显然也会检查对象是否需要初始化,但随后会使用第一个调用(rbx)中的缓存地址!所以如果它初始化了,那可能是错的?
下面是godbolt上clang 16的输出,它与gcc相当-相同的重新初始化测试和缓存地址,但它比我现在使用-fPIC的clang 10好一些。https://godbolt.org/z/M8PW1obE7

  1. main: # @main
  2. push rbx
  3. cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
  4. je .LBB0_2
  5. call TLS init function for ThreadStateLogger::instance@PLT
  6. .LBB0_2:
  7. data16
  8. lea rdi, [rip + ThreadStateLogger::instance@TLSGD]
  9. data16
  10. data16
  11. rex64
  12. call __tls_get_addr@PLT
  13. mov rbx, rax
  14. mov rax, qword ptr [rax + 8]
  15. mov byte ptr [rax], 97
  16. cmp byte ptr [rbx + 3], 2
  17. jae .LBB0_3
  18. cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
  19. jne .LBB0_5
  20. .LBB0_6:
  21. mov rax, qword ptr [rbx + 8]
  22. mov byte ptr [rax], 99
  23. cmp byte ptr [rbx + 3], 2
  24. jae .LBB0_7
  25. .LBB0_8:
  26. xor eax, eax
  27. pop rbx
  28. ret
  29. .LBB0_3:
  30. mov rax, qword ptr [rbx + 8]
  31. mov byte ptr [rax + 1], 98
  32. cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0
  33. je .LBB0_6
  34. .LBB0_5:
  35. call TLS init function for ThreadStateLogger::instance@PLT
  36. mov rax, qword ptr [rbx + 8]
  37. mov byte ptr [rax], 99
  38. cmp byte ptr [rbx + 3], 2
  39. jb .LBB0_8
  40. .LBB0_7:
  41. mov rax, qword ptr [rbx + 8]
  42. mov byte ptr [rax + 1], 100
  43. xor eax, eax
  44. pop rbx
  45. ret
  • 编辑 *

删除printf -?添加了更多的初始化检查,但是__tls_get_addr仍然缓存在rbx中。

  • 这看起来像是在至少1个代码路径中优化与TLS_numBytes的比较。我想这是因为我在两个调用之间没有复杂性,所以这看起来是一个有效的优化,但在实践中没有用处。

因此,运行时成本只是重复检查TLS-singleton-initialised标志,我们知道该标志已设置。还有代码膨胀,这很烦人。

  • tldr*

所以提醒一下这个问题:为什么重复初始化检查/调用?如果这是必要的,那么为什么不需要重新生成地址?或者这只是一个没有人想到要做的优化?有没有办法通过改变代码模式来获得更好的优化?我有几个去到这里,正如你所看到的。
当然,我需要一个具有这种行为的日志类的功能,并且使用一个类来封装3个TLS变量意味着它们都是一起构建的,而不是一次一个。

igsr9ssn

igsr9ssn1#

LLVM和GCC可能无法优化这一点。去虚拟化、线程本地存储初始化和其他语言特性可以在函数中进行本地优化,但是一旦它们被内联到另一个函数中,就没有进一步的潜力了。
当我们查看LLVM IR时,这个问题就变得很明显了:

  1. static void Log(char c) {
  2. // we suffer the same issue with one and two chars, this just makes our IR shorter
  3. instance.Log(c);
  4. }

在所有优化通过后,此函数变为:

  1. define linkonce_odr void @ThreadStateLogger::Log(char)(i8 noundef signext %v1) local_unnamed_addr #1 comdat align 2 {
  2. entry:
  3. br i1 icmp ne (ptr @TLS init function for ThreadStateLogger::instance, ptr null), label %0, label %TLS wrapper function for ThreadStateLogger::instance.exit
  4. 0: ; preds = %entry
  5. call void @TLS init function for ThreadStateLogger::instance()
  6. br label %TLS wrapper function for ThreadStateLogger::instance.exit
  7. TLS wrapper function for ThreadStateLogger::instance.exit: ; preds = %entry, %0
  8. %1 = call align 8 ptr @llvm.threadlocal.address.p0(ptr align 8 @ThreadStateLogger::instance)
  9. %TLS_byteLocPtr.i = getelementptr inbounds %"struct.ThreadStateLogger::Instance", ptr %1, i64 0, i32 2
  10. %2 = load ptr, ptr %TLS_byteLocPtr.i, align 8
  11. store i8 %v1, ptr %2, align 1
  12. ret void
  13. }

重要的是这个函数包含:

  • call void @TLS init function for ThreadStateLogger::instance()(断开)
  • call void @_ZTHN17ThreadStateLogger8instanceE()(损坏)

它不是一个神奇的函数,编译器无法知道调用它一次会使以后的调用变得不必要。函数调用位于从可变全局内存读取的分支中:

  1. cmp qword ptr [rip + _ZTHN17ThreadStateLogger8instanceE@GOTPCREL], 0

并且没有IR级别的信息,该存储器必须由我们的call void @_ZTHN17ThreadStateLogger8instanceE()设置为!= 0
如果随后将ThreadStateLogger::Log(char)内联到main中,则包含此函数调用的两个分支不会相互废弃,您将看到call TLS init function for ThreadStateLogger::instance@PLT的两个示例

__tls_get_addr为什么要优化?

__tls_get_addr@llvm.threadlocal.address.p0,它可能会受到对_ZTHN17ThreadStateLogger8instanceE函数的“随机”调用所没有的优化。
main中执行 EarlyCSEPass 时,该调用将被消除。参见compiler explorer with LLVM Optimization Pipeline

展开查看全部

相关问题