Каково снижение производительности локальных переменных потока C++11 в GCC 4.8?


С проект на GCC 4.8 в changelog:

G++ теперь реализует C++11thread_local ключевое слово; это отличается от ГНУ __thread ключевое слово в первую очередь в том, что она позволяет динамически семантика инициализации и разрушения. К сожалению, эта поддержка требуется штраф во время выполнения для ссылок на нефункциональные локальные thread_local переменные, даже если они не нуждаются в динамической инициализации, поэтому пользователи могут продолжать использовать __thread для TLS переменные с семантика статической инициализации.

какова именно природа и происхождение этого штрафа во время выполнения?

очевидно, для поддержки не-функции-local thread_local переменные там должна быть фаза инициализации потока перед входом в каждый поток main (так же, как есть статическая фаза инициализации для глобальных переменных), но они ссылаются на некоторые штрафы во время выполнения за это?

грубо говоря, что архитектура новая реализация GCC thread_local?

3 57

3 ответа:

(отказ от ответственности: я не знаю много о внутренностях GCC, так что это также образованная догадка.)

динамический thread_local инициализация добавлена в commit 462819c. Одним из изменений является:

* semantics.c (finish_id_expression): Replace use of thread_local
variable with a call to its wrapper.

таким образом, штраф за время выполнения заключается в том, что каждая ссылка thread_local переменная станет вызовом функции. Давайте проверим с помощью простого теста:

// 3.cpp
extern thread_local int tls;    
int main() {
    tls += 37;   // line 6
    tls &= 11;   // line 7
    tls ^= 3;    // line 8
    return 0;
}

// 4.cpp

thread_local int tls = 42;

когда скомпилированный*, мы видим, что использование tls ссылка становится вызовом функции _ZTW3tls, которые лениво инициализируют переменную один раз:

00000000004005b0 <main>:
main():
  4005b0:   55                          push   rbp
  4005b1:   48 89 e5                    mov    rbp,rsp
  4005b4:   e8 26 00 00 00              call   4005df <_ZTW3tls>    // line 6
  4005b9:   8b 10                       mov    edx,DWORD PTR [rax]
  4005bb:   83 c2 25                    add    edx,0x25
  4005be:   89 10                       mov    DWORD PTR [rax],edx
  4005c0:   e8 1a 00 00 00              call   4005df <_ZTW3tls>    // line 7
  4005c5:   8b 10                       mov    edx,DWORD PTR [rax]
  4005c7:   83 e2 0b                    and    edx,0xb
  4005ca:   89 10                       mov    DWORD PTR [rax],edx
  4005cc:   e8 0e 00 00 00              call   4005df <_ZTW3tls>    // line 8
  4005d1:   8b 10                       mov    edx,DWORD PTR [rax]
  4005d3:   83 f2 03                    xor    edx,0x3
  4005d6:   89 10                       mov    DWORD PTR [rax],edx
  4005d8:   b8 00 00 00 00              mov    eax,0x0              // line 9
  4005dd:   5d                          pop    rbp
  4005de:   c3                          ret

00000000004005df <_ZTW3tls>:
_ZTW3tls():
  4005df:   55                          push   rbp
  4005e0:   48 89 e5                    mov    rbp,rsp
  4005e3:   b8 00 00 00 00              mov    eax,0x0
  4005e8:   48 85 c0                    test   rax,rax
  4005eb:   74 05                       je     4005f2 <_ZTW3tls+0x13>
  4005ed:   e8 0e fa bf ff              call   0 <tls> // initialize the TLS
  4005f2:   64 48 8b 14 25 00 00 00 00  mov    rdx,QWORD PTR fs:0x0
  4005fb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  400602:   48 01 d0                    add    rax,rdx
  400605:   5d                          pop    rbp
  400606:   c3                          ret

сравните его с __thread версия, которая не будет иметь эту дополнительную обертку:

00000000004005b0 <main>:
main():
  4005b0:   55                          push   rbp
  4005b1:   48 89 e5                    mov    rbp,rsp
  4005b4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 6
  4005bb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005be:   8d 50 25                    lea    edx,[rax+0x25]
  4005c1:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005c8:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005cb:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 7
  4005d2:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005d5:   89 c2                       mov    edx,eax
  4005d7:   83 e2 0b                    and    edx,0xb
  4005da:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005e1:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005e4:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc // line 8
  4005eb:   64 8b 00                    mov    eax,DWORD PTR fs:[rax]
  4005ee:   89 c2                       mov    edx,eax
  4005f0:   83 f2 03                    xor    edx,0x3
  4005f3:   48 c7 c0 fc ff ff ff        mov    rax,0xfffffffffffffffc
  4005fa:   64 89 10                    mov    DWORD PTR fs:[rax],edx
  4005fd:   b8 00 00 00 00              mov    eax,0x0                // line 9
  400602:   5d                          pop    rbp
  400603:   c3                          ret

эта обертка не нужна в каждом случае использования thread_local хотя. Об этом можно узнать из decl2.c. Оболочка генерируется только тогда, когда:

  • это это не

Если переменная определена в текущем TU, то inliner позаботится о накладных расходах. Я ожидаю, что это будет верно для большинства применений thread_local.

для внешних переменных, если программист может быть уверен, что никакое использование переменной в не определяющем TU не должно инициализировать динамическую инициализацию (либо потому, что переменная статически инициализирована, либо использование переменной в определяющем TU будет выполнено до любого использования в другом TU), они могут избежать этих накладных расходов с опцией-fno-extern-tls-init.

C++11 thread_local имеет тот же эффект выполнения, что и спецификатор _ _ thread (__thread не является частью стандарта C; thread_local является частью стандарта C++)

это зависит от того, где переменная TLS (объявленная с __thread описатель) объявляется.

  • если переменная TLS объявлена в исполняемом файле, то доступ быстрый
  • если переменная TLS объявлена в коде общей библиотеки (скомпилирована с -fPIC опции компилятора) и -ftls-model=initial-exec опции компилятора указывается, что доступ быстрый; однако применяется следующее ограничение: общая библиотека не может быть загружена через dlopen / dlsym (динамическая загрузка), единственный способ использования библиотеки-связать с ней во время компиляции (опция компоновщика -l<libraryname>)
  • если переменная TLS объявлена в общей библиотеке (-fPIC compiler option set) тогда доступ очень медленный, так как предполагается общая динамическая модель TLS - здесь каждый доступ к переменной TLS приводит к вызову _tls_get_addr() ; это случай по умолчанию, потому что вы не ограничены в том, как используется общая библиотека.

источники: обработка ELF для локального хранения потоков Ульрихом Дреппером https://www.akkadia.org/drepper/tls.pdf этот текст также содержит код, созданный для поддерживаемых целевых платформ.