Производительность переменной ThreadLocal

86

Насколько из ThreadLocalпеременной читается медленнее, чем из обычного поля?

Более конкретно, создание простого объекта быстрее или медленнее, чем доступ к ThreadLocalпеременной?

Я предполагаю, что это достаточно быстро, так что наличие ThreadLocal<MessageDigest>экземпляра намного быстрее, чем создание экземпляра MessageDigestкаждый раз. Но применимо ли это также, например, к байту [10] или байту [1000]?

Изменить: вопрос в том, что на самом деле происходит при вызове ThreadLocalget? Если это просто поле, как и любое другое, тогда ответ будет «это всегда самое быстрое», верно?

Сармун
источник
2
Локальный поток - это в основном поле, содержащее хэш-карту и поиск, где ключом является текущий объект потока. Поэтому он намного медленнее, но все же быстро. :)
eckes
1
@eckes: он определенно ведет себя так, но обычно так не реализуется. Вместо этого Threads содержат (несинхронизированную) хэш-карту, где ключом является текущий ThreadLocalобъект
sbk

Ответы:

40

Выполнение неопубликованных тестов ThreadLocal.getна моей машине занимает около 35 циклов на итерацию. Ничего особенного. В реализации Sun настраиваемая хэш-карта линейного зондирования в Threadсопоставлении ThreadLocals со значениями. Поскольку к нему всегда обращается только один поток, он может быть очень быстрым.

Выделение небольших объектов занимает примерно такое же количество циклов, хотя из-за исчерпания кеша вы можете получить несколько меньшие цифры в жестком цикле.

Строительство, MessageDigestвероятно, будет относительно дорогим. Он имеет изрядное количество состояний и строительство проходит через Providerмеханизм SPI. Вы можете оптимизировать, например, клонируя или предоставляя Provider.

Тот ThreadLocalфакт, что кэширование в a может быть быстрее , чем создание, не обязательно означает, что производительность системы повысится. У вас будут дополнительные накладные расходы, связанные с сборщиком мусора, который все замедляет.

Если ваше приложение не очень сильно использует, MessageDigestвы можете вместо этого рассмотреть возможность использования обычного поточно-безопасного кеша.

Том Хотин - tackline
источник
5
ИМХО, самый быстрый способ - просто игнорировать SPI и использовать что-то вроде new org.bouncycastle.crypto.digests.SHA1Digest(). Я совершенно уверен, что никакой кэш не сможет его победить.
maaartinus
57

В 2009 году некоторые JVM реализовали ThreadLocal с использованием несинхронизированной HashMap в объекте Thread.currentThread (). Это сделало его чрезвычайно быстрым (хотя, конечно, не таким быстрым, как использование обычного доступа к полю), а также обеспечило приведение в порядок объекта ThreadLocal после смерти Thread. Обновляя этот ответ в 2016 году, кажется, что большинство (все?) Новые JVM используют ThreadLocalMap с линейным зондированием. Я не уверен в их производительности, но не могу представить, что это значительно хуже, чем в более ранней реализации.

Конечно, new Object () в наши дни тоже работает очень быстро, и сборщики мусора также очень хороши для восстановления недолговечных объектов.

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

Билл Мичелл
источник
4
+1 за то, что это единственный ответ на вопрос.
cletus
Можете ли вы привести мне пример современной JVM, которая не использует линейное зондирование для ThreadLocalMap? Java 8 OpenJDK по-прежнему использует ThreadLocalMap с линейным зондированием. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick
1
@Karthick Извини, я не могу. Я писал это еще в 2009 году. Буду обновлять.
Билл Мичелл
34

Хороший вопрос, я задавал себе этот вопрос недавно. Чтобы дать вам точные цифры, приведенные ниже тесты (в Scala, скомпилированные практически с теми же байт-кодами, что и эквивалентный код Java):

var cnt: String = ""
val tlocal = new java.lang.ThreadLocal[String] {
  override def initialValue = ""
}

def loop_heap_write = {                                                                                                                           
  var i = 0                                                                                                                                       
  val until = totalwork / threadnum                                                                                                               
  while (i < until) {                                                                                                                             
    if (cnt ne "") cnt = "!"                                                                                                                      
    i += 1                                                                                                                                        
  }                                                                                                                                               
  cnt                                                                                                                                          
} 

def threadlocal = {
  var i = 0
  val until = totalwork / threadnum
  while (i < until) {
    if (tlocal.get eq null) i = until + i + 1
    i += 1
  }
  if (i > until) println("thread local value was null " + i)
}

доступные здесь , были выполнены на двухъядерных процессорах AMD 4x 2,8 ГГц и четырехъядерном i7 с гиперпоточностью (2,67 ГГц).

Это числа:

i7

Спецификации: Intel i7 2x четырехъядерный @ 2,67 ГГц Тест: scala.threads.ParallelTests

Название теста: loop_heap_read

Номер темы: 1 Всего тестов: 200

Время выполнения: (показаны последние 5) 9.0069 9.0036 9.0017 9.0084 9.0074 (среднее = 9,1034 мин. = 8,9986 макс. = 21,0306)

Номер темы: 2 Всего тестов: 200

Время выполнения: (показаны последние 5) 4,5563 4,7128 4,5663 4,5617 4,5724 (средн. = 4,6337 мин. = 4,5509 макс. = 13,9476)

Номер темы: 4 Всего тестов: 200

Время выполнения: (показаны последние 5) 2,3946 2,3979 2,3934 2,3937 2,3964 (среднее = 2,5113 мин. = 2,3884 макс. = 13,5496)

Номер темы: 8 Всего тестов: 200

Время выполнения: (показаны последние 5) 2,4479 2,4362 2,4323 2,4472 2,4383 (среднее = 2,5562 мин. = 2,4166 макс. = 10,3726)

Название теста: threadlocal

Номер темы: 1 Всего тестов: 200

Время выполнения: (показаны последние 5) 91,1741 90,8978 90,6181 90,6200 90,6113 (среднее = 91,0291 мин. = 90,6000 макс. = 129,7501)

Номер темы: 2 Всего тестов: 200

Время выполнения: (показаны последние 5) 45,3838 45,3858 45,6676 45,3772 45,3839 (среднее = 46,0555 мин. = 45,3726 макс. = 90,7108)

Номер темы: 4 Всего тестов: 200

Время выполнения: (показаны последние 5) 22,8118 22,8135 59,1753 22,8229 22,8172 (среднее = 23,9752 мин. = 22,7951 макс. = 59,1753)

Номер темы: 8 Всего тестов: 200

Время выполнения: (показаны последние 5) 22,2965 22,2415 22,3438 22,3109 22,4460 (среднее = 23,2676 мин. = 22,2346 макс. = 50,3583)

AMD

Технические характеристики: AMD 8220, четырехъядерный процессор, 2,8 ГГц Тест: scala.threads.ParallelTests

Название теста: loop_heap_read

Всего работ: 20000000 Номер темы: 1 Всего тестов: 200

Время выполнения: (показаны последние 5) 12,625 12,631 12,634 12,632 12,628 (среднее = 12,7333 мин. = 12,619 макс. = 26,698)

Название теста: loop_heap_read Всего работ: 20000000

Время выполнения: (показаны последние 5) 6,412 6,424 6,408 6,397 6,43 (средн. = 6,5367 мин. = 6,393 макс. = 19,716)

Номер темы: 4 Всего тестов: 200

Время выполнения: (показаны последние 5) 3,385 4,298 9,7 6,535 3,385 (сред. = 5,6079 мин. = 3,354 макс. = 21,603)

Номер темы: 8 Всего тестов: 200

Время выполнения: (показаны последние 5) 5,389 5,795 10,818 3,823 3,824 (среднее = 5,5810 мин. = 2,405 макс. = 19,755)

Название теста: threadlocal

Номер темы: 1 Всего тестов: 200

Время выполнения: (показаны последние 5) 200,217 207,335 200,241 207,342 200,23 (среднее = 202,2424 мин. = 200,184 макс. = 245,369)

Номер темы: 2 Всего тестов: 200

Время выполнения: (показаны последние 5) 100,208 100,199 100,211 103,781 100,215 (среднее = 102,2238 мин. = 100,192 макс. = 129,505)

Номер темы: 4 Всего тестов: 200

Время выполнения: (показаны последние 5) 62,101 67,629 62,087 52,021 55,766 (сред. = 65,6361 мин. = 50,282 макс. = 167,433)

Номер темы: 8 Всего тестов: 200

Время выполнения: (показаны последние 5) 40,672 74,301 34,434 41,549 28,119 (среднее = 54,7701 мин. = 28,119 макс. = 94,424)

Резюме

Локальный поток примерно в 10-20 раз больше чтения кучи. Также кажется, что он хорошо масштабируется в этой реализации JVM и в этих архитектурах с количеством процессоров.

axel22
источник
5
+1 Престижность за то, что он единственный, кто дает количественные результаты. Я немного скептически настроен, потому что эти тесты проводятся на Scala, но, как вы сказали, байт-коды Java должны быть похожими ...
Gravity
Благодарность! Этот цикл while дает практически тот же байт-код, что и соответствующий код Java. Однако на разных виртуальных машинах можно наблюдать разное время - это было протестировано на Sun JVM1.6.
axel22 01
Этот тестовый код не моделирует хороший вариант использования ThreadLocal. В первом методе: каждый поток будет иметь общее представление в памяти, строка не изменяется. Во втором методе вы оцениваете стоимость поиска в хеш-таблице, где строка является дизъюнктивной между всеми потоками.
Joelmob
Строка не изменяется, но она читается из памяти (запись "!"никогда не происходит) в первом методе - первый метод фактически эквивалентен Threadсозданию подкласса и предоставлению ему настраиваемого поля. Тест измеряет крайний крайний случай, когда все вычисления состоят из чтения локальной переменной / потока - реальные приложения могут не пострадать в зависимости от их шаблона доступа, но в худшем случае они будут вести себя, как указано выше.
axel22
4

Вот и еще одно испытание. Результаты показывают, что ThreadLocal работает немного медленнее, чем обычное поле, но в том же порядке. Примерно на 12% медленнее

public class Test {
private static final int N = 100000000;
private static int fieldExecTime = 0;
private static int threadLocalExecTime = 0;

public static void main(String[] args) throws InterruptedException {
    int execs = 10;
    for (int i = 0; i < execs; i++) {
        new FieldExample().run(i);
        new ThreadLocaldExample().run(i);
    }
    System.out.println("Field avg:"+(fieldExecTime / execs));
    System.out.println("ThreadLocal avg:"+(threadLocalExecTime / execs));
}

private static class FieldExample {
    private Map<String,String> map = new HashMap<String, String>();

    public void run(int z) {
        System.out.println(z+"-Running  field sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            map.put(s,"a");
            map.remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        fieldExecTime += t;
        System.out.println(z+"-End field sample:"+t);
    }
}

private static class ThreadLocaldExample{
    private ThreadLocal<Map<String,String>> myThreadLocal = new ThreadLocal<Map<String,String>>() {
        @Override protected Map<String, String> initialValue() {
            return new HashMap<String, String>();
        }
    };

    public void run(int z) {
        System.out.println(z+"-Running thread local sample");
        long start = System.currentTimeMillis();
        for (int i = 0; i < N; i++){
            String s = Integer.toString(i);
            myThreadLocal.get().put(s, "a");
            myThreadLocal.get().remove(s);
        }
        long end = System.currentTimeMillis();
        long t = (end - start);
        threadLocalExecTime += t;
        System.out.println(z+"-End thread local sample:"+t);
    }
}
}'

Выход:

0-Образец бегущего поля

0-End образец поля: 6044

0-Запуск локального потока

0-конечный образец резьбы: 6015

1-бегущий полевой образец

Пример 1-конечного поля: 5095

Локальный образец 1-Running thread

1-концевая резьба, местный образец: 5720

2-беговой полевой образец

2-конечный образец поля: 4842

2-выполняющийся локальный пример потока

2-концевая резьба, местный образец: 5835

3-беговой полевой образец

Пример 3-конечного поля: 4674

Локальный образец 3-выполняющегося потока

Местный образец 3-концевой резьбы: 5287

4-беговой полевой образец

4-конечный образец поля: 4849

Локальный образец 4-выполняющегося потока

Местный образец 4-концевой резьбы: 5309

5-беговой полевой образец

5-конечный образец поля: 4781

Локальный образец 5-выполняющегося потока

5-концевая резьба, местный образец: 5330

6-беговой полевой образец

6-End образец поля: 5294

Локальный образец 6-выполняющегося потока

6-концевая резьба, местный образец: 5511

7-беговой полевой образец

7-End образец поля: 5119

7-Запуск локального потока

7-концевая резьба, локальный образец: 5793

8-беговой полевой образец

8-End образец поля: 4977

8-выполняющийся локальный пример потока

8-концевая резьба, местный образец: 6374

9-беговой полевой образец

9-End образец поля: 4841

9-Локальный образец выполняющегося потока

9-концевая резьба, местный образец: 5471

Среднее поле: 5051

ThreadLocal в среднем: 5664

Env:

openjdk версия "1.8.0_131"

Процессор Intel® Core ™ i7-7500U @ 2,70 ГГц × 4

Ubuntu 16.04 LTS

джперейра
источник
Извините, это даже близко не является действительным тестом. A) Самая большая проблема: вы выделяете строки на каждой итерации ( Int.toString)что чрезвычайно дорого по сравнению с тем, что вы тестируете. B) вы выполняете две операции карты на каждой итерации, также совершенно несвязанные и дорогие. Попробуйте вместо этого увеличить примитивный int из ThreadLocal. C) Использование System.nanoTimeвместо System.currentTimeMillis, бывшее для профилирования, последнее для пользовательских целей даты и времени и может изменяться под ногами. D) Вам следует полностью избегать аллоки, включая аллоки верхнего уровня для ваших «примерных» классов
Филип Гуин
3

@Pete - правильный тест перед оптимизацией.

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

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

Гарет Дэвис
источник
0

Постройте и измерьте.

Кроме того, вам нужен только один threadlocal, если вы инкапсулируете поведение переваривания сообщения в объект. Если вам для какой-то цели нужны локальный MessageDigest и локальный байт [1000], создайте объект с полем messageDigest и byte [] и поместите этот объект в ThreadLocal, а не в оба по отдельности.

Пит Киркхэм
источник
Спасибо, MessageDigest и byte [] используются по-разному, поэтому один объект не нужен.
Сармун, 04