I only tested this on amd64, but see for yourself: + cat t.cc struct foo { foo(); ~foo(); }; foo *test() { static thread_local foo foo_tls; return &foo_tls; } + g++-7 -std=c++14 -v -pthread -fPIC -shared -O2 -o gcc.so t.cc Using built-in specs. COLLECT_GCC=/usr/bin/g++-7 COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Debian 7.1.0-9' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 7.1.0 (Debian 7.1.0-9) COLLECT_GCC_OPTIONS='-std=c++14' '-v' '-pthread' '-fPIC' '-shared' '-O2' '-o' 'gcc.so' '-shared-libgcc' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/7/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE -D_REENTRANT t.cc -quiet -dumpbase t.cc -mtune=generic -march=x86-64 -auxbase t -O2 -std=c++14 -version -fPIC -o /tmp/ccdUrCDS.s GNU C++14 (Debian 7.1.0-9) version 7.1.0 (x86_64-linux-gnu) compiled by GNU C version 7.1.0, GMP version 6.1.2, MPFR version 3.1.5, MPC version 1.0.3, isl version isl-0.18-GMP GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/7" ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/7/../../../../x86_64-linux-gnu/include" #include "..." search starts here: #include <...> search starts here: /usr/include/c++/7 /usr/include/x86_64-linux-gnu/c++/7 /usr/include/c++/7/backward /usr/lib/gcc/x86_64-linux-gnu/7/include /usr/local/include /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed /usr/include/x86_64-linux-gnu /usr/include End of search list. GNU C++14 (Debian 7.1.0-9) version 7.1.0 (x86_64-linux-gnu) compiled by GNU C version 7.1.0, GMP version 6.1.2, MPFR version 3.1.5, MPC version 1.0.3, isl version isl-0.18-GMP GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 Compiler executable checksum: 3681302eda59faba4e53a905eca4bf72 COLLECT_GCC_OPTIONS='-std=c++14' '-v' '-pthread' '-fPIC' '-shared' '-O2' '-o' 'gcc.so' '-shared-libgcc' '-mtune=generic' '-march=x86-64' as -v --64 -o /tmp/ccI2B3TO.o /tmp/ccdUrCDS.s GNU assembler version 2.28 (x86_64-linux-gnu) using BFD version (GNU Binutils for Debian) 2.28 COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/ LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../:/lib/:/usr/lib/ COLLECT_GCC_OPTIONS='-std=c++14' '-v' '-pthread' '-fPIC' '-shared' '-O2' '-o' 'gcc.so' '-shared-libgcc' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/cc9S0zbL.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lpthread -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu -shared -o gcc.so /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. /tmp/ccI2B3TO.o -lstdc++ -lm -lgcc_s -lpthread -lc -lgcc_s /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o COLLECT_GCC_OPTIONS='-std=c++14' '-v' '-pthread' '-fPIC' '-shared' '-O2' '-o' 'gcc.so' '-shared-libgcc' '-mtune=generic' '-march=x86-64' + gdb -q -ex disassemble test -ex quit gcc.so Reading symbols from gcc.so...(no debugging symbols found)...done. Dump of assembler code for function _Z4testv: 0x00000000000007f0 <+0>: push %rbx 0x00000000000007f1 <+1>: sub $0x10,%rsp 0x00000000000007f5 <+5>: lea 0x2007cc(%rip),%rdi # 0x200fc8 0x00000000000007fc <+12>: callq 0x6e0 <__tls_get_addr@plt> 0x0000000000000801 <+17>: cmpb $0x0,0x0(%rax) 0x0000000000000808 <+24>: jne 0x840 <_Z4testv+80> 0x000000000000080a <+26>: lea 0x8(%rax),%rbx 0x0000000000000811 <+33>: mov %rax,0x8(%rsp) 0x0000000000000816 <+38>: mov %rbx,%rdi 0x0000000000000819 <+41>: callq 0x6d0 <_ZN3fooC1Ev@plt> 0x000000000000081e <+46>: mov 0x8(%rsp),%rax 0x0000000000000823 <+51>: mov 0x2007b6(%rip),%rdi # 0x200fe0 0x000000000000082a <+58>: lea 0x2007ff(%rip),%rdx # 0x201030 0x0000000000000831 <+65>: mov %rbx,%rsi 0x0000000000000834 <+68>: movb $0x1,0x0(%rax) 0x000000000000083b <+75>: callq 0x6f0 <__cxa_thread_atexit@plt> 0x0000000000000840 <+80>: lea 0x200781(%rip),%rdi # 0x200fc8 0x0000000000000847 <+87>: callq 0x6e0 <__tls_get_addr@plt> 0x000000000000084c <+92>: add $0x10,%rsp 0x0000000000000850 <+96>: add $0x8,%rax 0x0000000000000856 <+102>: pop %rbx 0x0000000000000857 <+103>: retq End of assembler dump. As you can see after the first call to __tls_get_addr() jne jumps to a second call to __tls_get_addr(). It should really only need to get the address once here, like clang does: + clang++ -std=c++14 -pthread -fPIC -shared -O2 -o clang.so t.cc + gdb -q -ex disassemble test -ex quit clang.so Reading symbols from clang.so...(no debugging symbols found)...done. Dump of assembler code for function _Z4testv: 0x00000000000007a0 <+0>: push %r14 0x00000000000007a2 <+2>: push %rbx 0x00000000000007a3 <+3>: push %rax 0x00000000000007a4 <+4>: lea 0x20081d(%rip),%rdi # 0x200fc8 0x00000000000007ab <+11>: callq 0x690 <__tls_get_addr@plt> 0x00000000000007b0 <+16>: mov %rax,%rbx 0x00000000000007b3 <+19>: mov 0x1(%rax),%al 0x00000000000007b9 <+25>: and $0x1,%al 0x00000000000007bb <+27>: jne 0x7ef <_Z4testv+79> 0x00000000000007bd <+29>: mov %rbx,%rax 0x00000000000007c0 <+32>: lea 0x0(%rax),%r14 0x00000000000007c7 <+39>: mov %r14,%rdi 0x00000000000007ca <+42>: callq 0x680 <_ZN3fooC1Ev@plt> 0x00000000000007cf <+47>: mov 0x20080a(%rip),%rdi # 0x200fe0 0x00000000000007d6 <+54>: lea 0x200853(%rip),%rdx # 0x201030 0x00000000000007dd <+61>: mov %r14,%rsi 0x00000000000007e0 <+64>: callq 0x6a0 <__cxa_thread_atexit@plt> 0x00000000000007e5 <+69>: mov %rbx,%rax 0x00000000000007e8 <+72>: movb $0x1,0x1(%rax) 0x00000000000007ef <+79>: mov %rbx,%rax 0x00000000000007f2 <+82>: lea 0x0(%rax),%rax 0x00000000000007f9 <+89>: add $0x8,%rsp 0x00000000000007fd <+93>: pop %rbx 0x00000000000007fe <+94>: pop %r14 0x0000000000000800 <+96>: retq End of assembler dump. This has some performance overhead which I'd like to avoid.
To qualify the performance overhead, I added empty constructors and destructors with noinline, and compiled the code with g++ and clang++, and then ran a loop 100000000 over the function. The clang code took 6 nanosecond, the g++ code 8 nanosecond per iteration, that's 33% worse. I think it's probably a neglectable overhead after all, but it seems like that would be a sensible and maybe an easy optimization to do.
.
See also bug #82803.
*** Bug 82803 has been marked as a duplicate of this bug. ***
Due to the way addresses are formed for TLS, we emit the call to the __tls_get_addr function at the point of address. We should do something similar to how other PIC addresses are handled.
We recently upgraded our toolchain from GCC9 to GCC11, and we're seeing __tls_get_addr take up to 10% of total runtime under some workloads, where it was 1-2% before. It seems that some changes to the optimization passes in 10 or 11 have significantly increased the impact of this problem.
(In reply to Roy Jacobson from comment #6) > We recently upgraded our toolchain from GCC9 to GCC11, and we're seeing > __tls_get_addr take up to 10% of total runtime under some workloads, where > it was 1-2% before. > > It seems that some changes to the optimization passes in 10 or 11 have > significantly increased the impact of this problem. In bug #82803, comment #12, (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82803#c12), I've shown a workaround I used, which might be useful until GCC handle __tls_get_addr() as returning a constant addresses that doesn't need to be looked up multiple times in a function.
Created attachment 53473 [details] A patch This patch uses a single UNSPEC_TLS_LD_BASE in the whole function.
Thanks for the patch! I wonder if it would handle coroutines correctly. Clang has this open bug https://github.com/llvm/llvm-project/issues/47179 that is related to this optimization.
This came up in https://yosefk.com/blog/cxx-thread-local-storage-performance.html today.
Created attachment 60535 [details] An untested patch
https://inbox.sourceware.org/gcc-patches/CAMe9rOqp-_2DFXbmAnWhxOA=KEpFme6o3APLcuj5M=tYZ-SrvQ@mail.gmail.com/
Created attachment 61788 [details] A patch
Created attachment 61790 [details] An updated patch This is the patch I will submit. It reduces the number of __tls_get_addr calls in libgcc.a by 72%: __tls_get_addr calls before after libgcc.a 868 243
The master branch has been updated by H.J. Lu <hjl@gcc.gnu.org>: https://gcc.gnu.org/g:5cf1b9a03ec5b617af8c50c1e9c0d223083fd7f2 commit r16-3190-g5cf1b9a03ec5b617af8c50c1e9c0d223083fd7f2 Author: H.J. Lu <hjl.tools@gmail.com> Date: Fri Aug 19 11:50:41 2022 -0700 x86-64: Remove redundant TLS calls For TLS calls: 1. UNSPEC_TLS_GD: (parallel [ (set (reg:DI 0 ax) (call:DI (mem:QI (symbol_ref:DI ("__tls_get_addr"))) (const_int 0 [0]))) (unspec:DI [(symbol_ref:DI ("e") [flags 0x50]) (reg/f:DI 7 sp)] UNSPEC_TLS_GD) (clobber (reg:DI 5 di))]) 2. UNSPEC_TLS_LD_BASE: (parallel [ (set (reg:DI 0 ax) (call:DI (mem:QI (symbol_ref:DI ("__tls_get_addr"))) (const_int 0 [0]))) (unspec:DI [(reg/f:DI 7 sp)] UNSPEC_TLS_LD_BASE)]) 3. UNSPEC_TLSDESC: (parallel [ (set (reg/f:DI 104) (plus:DI (unspec:DI [ (symbol_ref:DI ("_TLS_MODULE_BASE_") [flags 0x10]) (reg:DI 114) (reg/f:DI 7 sp)] UNSPEC_TLSDESC) (const:DI (unspec:DI [ (symbol_ref:DI ("e") [flags 0x1a]) ] UNSPEC_DTPOFF)))) (clobber (reg:CC 17 flags))]) (parallel [ (set (reg:DI 101) (unspec:DI [(symbol_ref:DI ("e") [flags 0x50]) (reg:DI 112) (reg/f:DI 7 sp)] UNSPEC_TLSDESC)) (clobber (reg:CC 17 flags))]) they return the same value for the same input value. But multiple calls with the same input value may be generated for simple programs like: void a(long *); int b(void); void c(void); static __thread long e; long d(void) { a(&e); if (b()) c(); return e; } When compiled with -O2 -fPIC -mtls-dialect=gnu2, the following codes are generated: .type d, @function d: .LFB0: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 leaq e@TLSDESC(%rip), %rbx movq %rbx, %rax call *e@TLSCALL(%rax) addq %fs:0, %rax movq %rax, %rdi call a@PLT call b@PLT testl %eax, %eax jne .L8 movq %rbx, %rax call *e@TLSCALL(%rax) popq %rbx .cfi_remember_state .cfi_def_cfa_offset 8 movq %fs:(%rax), %rax ret .p2align 4,,10 .p2align 3 .L8: .cfi_restore_state call c@PLT movq %rbx, %rax call *e@TLSCALL(%rax) popq %rbx .cfi_def_cfa_offset 8 movq %fs:(%rax), %rax ret .cfi_endproc There are 3 "call *e@TLSCALL(%rax)". They all return the same value. Rename the remove_redundant_vector pass to the x86_cse pass, for 64bit, extend it to also remove redundant TLS calls to generate: d: .LFB0: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 leaq e@TLSDESC(%rip), %rax movq %fs:0, %rdi call *e@TLSCALL(%rax) addq %rax, %rdi movq %rax, %rbx call a@PLT call b@PLT testl %eax, %eax jne .L8 movq %fs:(%rbx), %rax popq %rbx .cfi_remember_state .cfi_def_cfa_offset 8 ret .p2align 4,,10 .p2align 3 .L8: .cfi_restore_state call c@PLT movq %fs:(%rbx), %rax popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc with only one "call *e@TLSCALL(%rax)". This reduces the number of __tls_get_addr calls in libgcc.a by 72%: __tls_get_addr calls before after libgcc.a 868 243 gcc/ PR target/81501 * config/i386/i386-features.cc (x86_cse_kind): Add X86_CSE_TLS_GD, X86_CSE_TLS_LD_BASE and X86_CSE_TLSDESC. (redundant_load): Renamed to ... (redundant_pattern): This. (ix86_place_single_vector_set): Replace redundant_load with redundant_pattern. (replace_tls_call): New. (ix86_place_single_tls_call): Likewise. (pass_remove_redundant_vector_load): Renamed to ... (pass_x86_cse): This. Add val, def_insn, mode, scalar_mode, kind, x86_cse, candidate_gnu_tls_p, candidate_gnu2_tls_p and candidate_vector_p. (pass_x86_cse::candidate_gnu_tls_p): New. (pass_x86_cse::candidate_gnu2_tls_p): Likewise. (pass_x86_cse::candidate_vector_p): Likewise. (remove_redundant_vector_load): Renamed to ... (pass_x86_cse::x86_cse): This. Extend to remove redundant TLS calls. (make_pass_remove_redundant_vector_load): Renamed to ... (make_pass_x86_cse): This. * config/i386/i386-passes.def: Replace pass_remove_redundant_vector_load with pass_x86_cse. * config/i386/i386-protos.h (ix86_tls_get_addr): New. (make_pass_remove_redundant_vector_load): Renamed to ... (make_pass_x86_cse): This. * config/i386/i386.cc (ix86_tls_get_addr): Remove static. * config/i386/i386.h (machine_function): Add tls_descriptor_call_multiple_p. * config/i386/i386.md (tls64): New attribute. (@tls_global_dynamic_64_<mode>): Set tls_descriptor_call_multiple_p. (@tls_local_dynamic_base_64_<mode>): Likewise. (@tls_dynamic_gnu2_64_<mode>): Likewise. (*tls_global_dynamic_64_<mode>): Set tls64 attribute to gd. (*tls_local_dynamic_base_64_<mode>): Set tls64 attribute to ld_base. (*tls_dynamic_gnu2_lea_64_<mode>): Set tls64 attribute to lea. (*tls_dynamic_gnu2_call_64_<mode>): Set tls64 attribute to call. (*tls_dynamic_gnu2_combine_64_<mode>): Set tls64 attribute to combine. gcc/testsuite/ PR target/81501 * g++.target/i386/pr81501-1.C: New test. * gcc.target/i386/pr81501-1a.c: Likewise. * gcc.target/i386/pr81501-1b.c: Likewise. * gcc.target/i386/pr81501-2a.c: Likewise. * gcc.target/i386/pr81501-2b.c: Likewise. * gcc.target/i386/pr81501-3.c: Likewise. * gcc.target/i386/pr81501-4a.c: Likewise. * gcc.target/i386/pr81501-4b.c: Likewise. * gcc.target/i386/pr81501-5.c: Likewise. * gcc.target/i386/pr81501-6a.c: Likewise. * gcc.target/i386/pr81501-6b.c: Likewise. * gcc.target/i386/pr81501-7.c: Likewise. * gcc.target/i386/pr81501-8a.c: Likewise. * gcc.target/i386/pr81501-8b.c: Likewise. * gcc.target/i386/pr81501-9a.c: Likewise. * gcc.target/i386/pr81501-9b.c: Likewise. * gcc.target/i386/pr81501-10a.c: Likewise. * gcc.target/i386/pr81501-10b.c: Likewise. Signed-off-by: H.J. Lu <hjl.tools@gmail.com>
Fixed in GCC 16 for x86-64. I am enclosing this bug. Please open a new bug report for aarch64 if needed.