Подтвердить что ты не робот

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

Из GCC 4.8 черновика изменений:

g++ теперь реализует ключевое слово С++ 11 thread_local; это отличается от ключевое слово GNU __thread прежде всего тем, что оно позволяет динамически семантика инициализации и уничтожения. К сожалению, эта поддержка требуется штраф во время выполнения для ссылок на не-функционально-локальные thread_local, даже если им не нужна динамическая инициализация, поэтому пользователи могут продолжать использовать __thread для переменных TLS с статическая семантика инициализации.

В чем же суть и происхождение этого временного штрафа?

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

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

4b9b3361

Ответ 1

(Отказ от ответственности: я мало знаю о внутренних компонентах 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. Обертка создается только тогда, когда:

  • Это не функция-локальная, а

    • Это extern (пример показан выше) или
    • Тип имеет нетривиальный деструктор (который не допускается для переменных __thread) или
    • Тип переменной инициализируется не константным выражением (которое также не допускается для переменных __thread).

Во всех других случаях использования он ведет себя так же, как __thread. Это означает, что, если у вас нет переменных extern __thread, вы можете заменить все __thread на thread_local без потери производительности.


*: Я скомпилирован с -O0, потому что inliner сделает границу функции менее заметной. Даже если мы переходим к -O3, эти проверки инициализации все еще остаются.

Ответ 2

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

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

Ответ 3

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

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

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

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