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

Почему локальное хранилище потоков так медленно?

Я работаю над настраиваемым распределением памяти стиля стилей mark-release для языка программирования D, который работает путем выделения из локальных областей потока. Похоже, что узкое место в локальном хранилище потоков вызывает значительное (~ 50%) замедление в распределении памяти из этих регионов по сравнению с идентичной однопоточной версией кода, даже после разработки моего кода, чтобы иметь только один поиск TLS для распределения/открепление. Это основано на многократном распределении/освобождении памяти в цикле, и я пытаюсь выяснить, является ли это артефактом моего метода бенчмаркинга. Я понимаю, что локальное хранилище потоков должно в основном включать доступ к чему-либо через дополнительный слой косвенности, подобно доступу к переменной с помощью указателя. Это неверно? Сколько накладных расходов обычно имеется в потоковом локальном хранилище?

Примечание. Хотя я упоминаю D, меня также интересуют общие ответы, которые не относятся к D, поскольку реализация D-потоковой локальной памяти, скорее всего, улучшится, если она будет медленнее, чем лучшие реализации.

4b9b3361

Ответ 1

Скорость зависит от реализации TLS.

Да, вы правы, что TLS может быть таким же быстрым, как поиск указателя. Это может быть даже быстрее в системах с блоком управления памятью.

Для поиска указателя вам нужна помощь от планировщика. Планировщик должен - на коммутаторе задачи - обновить указатель на данные TLS.

Еще один быстрый способ реализации TLS - через модуль управления памятью. Здесь TLS обрабатывается как любые другие данные, за исключением того, что переменные TLS выделяются в специальном сегменте. Планировщик будет - на переключателе задач - отобразить правильный фрагмент памяти в адресное пространство задачи.

Если планировщик не поддерживает какой-либо из этих методов, компилятор/библиотека должен сделать следующее:

  • получить текущий ThreadId
  • Возьмите семафор
  • Посмотрите указатель на блок TLS с помощью ThreadId (может использовать карту или так)
  • Отпустите семафор
  • Возвращает этот указатель.

Очевидно, что все это для каждого доступа к данным TLS занимает некоторое время и может потребоваться до трех вызовов ОС: Получение ThreadId, принятие и освобождение семафора.

Семафор необходим, чтобы убедиться, что ни один поток не читается из списка указателей TLS, в то время как другой поток находится в середине размножения нового потока. (и как таковые выделяют новый блок TLS и изменяют структуру данных).

К сожалению, нередко можно увидеть медленную реализацию TLS на практике.

Ответ 2

Местные жители в D очень быстры. Вот мои тесты.

64-разрядный Ubuntu, ядро ​​i5, dmd v2.052 Параметры компилятора: dmd -O -release -inline -m64

// this loop takes 0m0.630s
void main(){
    int a; // register allocated
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

// this loop takes 0m1.875s
int a; // thread local in D, not static
void main(){
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

Таким образом, мы теряем только 1,2 секунды одного из ядер ЦП на локальный доступ 1000 * 1000 * 1000 потоков. Доступ к локалям потоков осуществляется с помощью регистра% fs, поэтому задействовано только несколько команд процессора:

Разборка с помощью objdump -d:

- this is local variable in %ecx register (loop counter in %eax):
   8:   31 c9                   xor    %ecx,%ecx
   a:   b8 00 ca 9a 3b          mov    $0x3b9aca00,%eax
   f:   83 c1 09                add    $0x9,%ecx
  12:   ff c8                   dec    %eax
  14:   85 c0                   test   %eax,%eax
  16:   75 f7                   jne    f <_Dmain+0xf>

- this is thread local, %fs register is used for indirection, %edx is loop counter:
   6:   ba 00 ca 9a 3b          mov    $0x3b9aca00,%edx
   b:   64 48 8b 04 25 00 00    mov    %fs:0x0,%rax
  12:   00 00 
  14:   48 8b 0d 00 00 00 00    mov    0x0(%rip),%rcx        # 1b <_Dmain+0x1b>
  1b:   83 04 08 09             addl   $0x9,(%rax,%rcx,1)
  1f:   ff ca                   dec    %edx
  21:   85 d2                   test   %edx,%edx
  23:   75 e6                   jne    b <_Dmain+0xb>

Возможно, компилятор может быть еще более умным, а кеш-поток локализован до цикла в регистр и вернуть его в поток local в конце (интересно сравнить с gdc-компилятором), но даже сейчас вопросы очень хорошие ИМХО.

Ответ 3

Нужно быть очень осторожным в интерпретации результатов тестов. Например, недавний поток в группах новостей D закончился из теста, что генерация кода dmd вызывала серьезное замедление в цикле, которое делало арифметику, но на самом деле в расписании времени доминировала вспомогательная функция времени выполнения, которая выполняла длинное деление. Генерация кода компилятора не имела ничего общего с замедлением.

Чтобы узнать, какой код генерируется для tls, скомпилируйте и obj2asm этот код:

__thread int x;
int foo() { return x; }

TLS реализована по-разному в Windows, чем в Linux, и будет сильно отличаться от OSX. Но во всех случаях это будет намного больше инструкций, чем простая загрузка статической ячейки памяти. TLS всегда будет медленным относительно простого доступа. Доступ к глобалам TLS в узком цикле также будет медленным. Попробуйте кэшировать значение TLS вместо временного.

Я написал код распределения пула потоков несколько лет назад и кэшировал дескриптор TLS в пул, который работал хорошо.

Ответ 4

Если вы не можете использовать поддержку TLS компилятора, вы можете самостоятельно управлять TLS. Я построил шаблон оболочки для С++, поэтому легко заменить базовую реализацию. В этом примере я реализовал его для Win32. Примечание. Поскольку вы не можете получить неограниченное количество индексов TLS для каждого процесса (по крайней мере, под Win32) вы должны указать на блоки кучи, достаточно большие, чтобы хранить все данные, зависящие от потока. Таким образом, вы имеете минимальное количество индексов TLS и связанных запросов. В "наилучшем случае" у вас будет всего один указатель TLS, указывающий на один отдельный блок кучи на поток.

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

Не забывайте освобождать память, если она не используется снова. Я делаю это, обертывая поток в класс (например, Java) и обрабатывая TLS конструктором и деструктором. Кроме того, я храню часто используемые данные, такие как ручки потоков и идентификатор в качестве членов класса.

Использование:

для типа *: tl_ptr < тип >

для типа const *: tl_ptr < const type >

для типа * const: const tl_ptr <type>

const type * const: const tl_ptr < const type >

template<typename T>
class tl_ptr {
protected:
    DWORD index;
public:
    tl_ptr(void) : index(TlsAlloc()){
        assert(index != TLS_OUT_OF_INDEXES);
        set(NULL);
    }
    void set(T* ptr){
        TlsSetValue(index,(LPVOID) ptr);
    }
    T* get(void)const {
        return (T*) TlsGetValue(index);
    }
    tl_ptr& operator=(T* ptr){
        set(ptr);
        return *this;
    }
    tl_ptr& operator=(const tl_ptr& other){
        set(other.get());
        return *this;
    }
    T& operator*(void)const{
        return *get();
    }
    T* operator->(void)const{
        return get();
    }
    ~tl_ptr(){
        TlsFree(index);
    }
};

Ответ 5

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

Такой подход хорошо работает, если вы не возражаете, чтобы пространство для каждой локальной локальной переменной было выделено в каждом потоке - даже те, которые на самом деле никогда его не используют, - и если все, что будет в локальном хранилище потоков блок может быть определен как одна структура. В этом случае доступ к потоковым локальным переменным может быть почти таким же быстрым, как доступ к другим переменным, и только различие между ними является дополнительным разыменованием указателя. К сожалению, многие приложения для ПК требуют чего-то более сложного.

В некоторых фреймворках для ПК поток будет иметь место только для потоков-статических переменных, если модуль, который использует эти переменные, был запущен в этом потоке. Хотя иногда это может быть выгодно, это означает, что разные потоки часто имеют локальное хранилище, выложенное по-разному. Следовательно, может потребоваться, чтобы потоки имели какой-то индекс для поиска, где находятся их переменные, и направлять все обращения к этим переменным через этот индекс.

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

Ответ 6

Мы видели аналогичные проблемы с производительностью TLS (в Windows). Мы полагаемся на это для некоторых критических операций внутри нашего продукта "Ядро". После некоторых усилий я решил попробовать и улучшить это.

Я рад сказать, что теперь у нас есть небольшой API, который предлагает > 50% -ное сокращение времени процессора для эквивалентной операции, когда поток callin не "знает" свой поток-идентификатор и > 65% -ное сокращение, если вы звоните thread уже получил свой идентификатор потока (возможно, для некоторого другого более раннего этапа обработки).

Новая функция (get_thread_private_ptr()) всегда возвращает указатель на структуру, которую мы используем внутри, для хранения всех видов, поэтому нам нужен только один поток.

В целом я думаю, что поддержка Win32 TLS плохо разработана.