Bug 81501 - mulitple calls to __tls_get_addr() with -fPIC
Summary: mulitple calls to __tls_get_addr() with -fPIC
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: rtl-optimization (show other bugs)
Version: 7.1.0
: P3 enhancement
Target Milestone: 16.0
Assignee: H.J. Lu
URL:
Keywords: missed-optimization, patch
: 82803 (view as bug list)
Depends on:
Blocks:
 
Reported: 2017-07-20 21:10 UTC by Julian Andres Klode
Modified: 2025-08-13 19:46 UTC (History)
7 users (show)

See Also:
Host:
Target: x86_64-linux-gnu
Build:
Known to work:
Known to fail:
Last reconfirmed: 2017-11-02 00:00:00


Attachments
A patch (2.55 KB, patch)
2022-08-18 21:07 UTC, H.J. Lu
Details | Diff
An untested patch (4.59 KB, patch)
2025-02-19 22:58 UTC, H.J. Lu
Details | Diff
A patch (8.54 KB, patch)
2025-07-02 19:57 UTC, H.J. Lu
Details | Diff
An updated patch (9.82 KB, patch)
2025-07-03 00:09 UTC, H.J. Lu
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Julian Andres Klode 2017-07-20 21:10:26 UTC
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.
Comment 1 Julian Andres Klode 2017-07-20 21:26:27 UTC
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.
Comment 2 Drea Pinski 2017-11-02 06:26:30 UTC
.
Comment 3 Yann Droneaud 2019-09-20 14:03:51 UTC
See also bug #82803.
Comment 4 Drea Pinski 2021-08-22 08:26:49 UTC
*** Bug 82803 has been marked as a duplicate of this bug. ***
Comment 5 Drea Pinski 2021-08-22 08:30:01 UTC
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.
Comment 6 Roy Jacobson 2022-08-17 13:34:02 UTC
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.
Comment 7 Yann Droneaud 2022-08-17 15:44:50 UTC
(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.
Comment 8 H.J. Lu 2022-08-18 21:07:10 UTC
Created attachment 53473 [details]
A patch

This patch uses a single UNSPEC_TLS_LD_BASE in the whole function.
Comment 9 Roy Jacobson 2022-09-23 13:18:24 UTC
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.
Comment 10 Sam James 2025-02-18 01:10:38 UTC
This came up in https://yosefk.com/blog/cxx-thread-local-storage-performance.html today.
Comment 11 H.J. Lu 2025-02-19 22:58:27 UTC
Created attachment 60535 [details]
An untested patch
Comment 13 H.J. Lu 2025-07-02 19:57:20 UTC
Created attachment 61788 [details]
A patch
Comment 14 H.J. Lu 2025-07-03 00:09:46 UTC
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
Comment 15 GCC Commits 2025-08-13 19:36:27 UTC
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>
Comment 16 H.J. Lu 2025-08-13 19:46:07 UTC
Fixed in GCC 16 for x86-64.  I am enclosing this bug.  Please open a new bug report
for aarch64 if needed.