Продуктивність змінної ThreadLocal


86

Скільки читається із ThreadLocalзмінної повільніше, ніж із звичайного поля?

Більш конкретно, просте створення об’єкта швидше чи повільніше, ніж доступ до ThreadLocalзмінної?

Я припускаю, що це досить швидко, так що наявність ThreadLocal<MessageDigest>примірника набагато швидше, ніж створення примірника MessageDigestкожного разу. Але чи стосується це, наприклад, байта [10] чи байта [1000]?

Редагувати: Питання в тому, що насправді відбувається під час дзвінка ThreadLocal? Якщо це просто поле, як і будь-яке інше, тоді відповідь буде "це завжди найшвидше", правда?


2
Локальний потік - це в основному поле, що містить хеш-карту та пошук, де ключем є поточний об’єкт потоку. Тому це набагато повільніше, але все одно швидко. :)
eckes

1
@eckes: він, звичайно, поводиться так, але зазвичай не реалізується таким чином. Натомість Threads містять (несинхронізовану) хеш-карту, де ключовим є поточний ThreadLocalоб’єкт
sbk

Відповіді:


40

Запуск неопублікованих тестів ThreadLocal.getзаймає близько 35 циклів за ітерацію на моїй машині. Не дуже багато. У реалізації Sun спеціальна лінійна зондована хеш-карта на Threadкартах ThreadLocals до значень. Оскільки до нього колись доступний лише один потік, це може бути дуже швидко.

Розподіл дрібних об'єктів займає однакову кількість циклів, хоча через вичерпання кеш-пам'яті ви можете отримати дещо нижчі цифри в щільному циклі.

Будівництво MessageDigest, ймовірно, буде відносно дорогим. У ньому досить багато штату, і будівництво проходить через Providerмеханізм SPI. Можливо, ви зможете оптимізувати, наприклад, клонуванням або наданням Provider.

Те, що це може бути швидше кешувати, ThreadLocalа не створювати, не обов’язково означає, що продуктивність системи зросте. Ви матимете додаткові накладні витрати, пов’язані з GC, що уповільнює все.

Якщо ваша програма не використовує інтенсивно, MessageDigestви можете розглянути можливість використання звичайного потокового кешу.


5
ІМХО, найшвидший спосіб - це просто ігнорувати SPI та використовувати щось на зразок new org.bouncycastle.crypto.digests.SHA1Digest(). Я повністю впевнений, що жодна кеш-пам’ять не може його перемогти.
maaartinus

57

У 2009 році деякі JVM реалізували ThreadLocal, використовуючи несинхронізовану HashMap в об'єкті Thread.currentThread (). Це зробило це надзвичайно швидким (хоча і не настільки швидким, як звичайний доступ до поля, звичайно), а також забезпечило приведення в порядок об’єкта ThreadLocal, коли Поток загинув. Оновляючи цю відповідь у 2016 році, здається, що більшість (усіх?) Новіших JVM використовують ThreadLocalMap з лінійним зондуванням. Я не впевнений щодо їх ефективності, але я не можу уявити, що це значно гірше, ніж попереднє впровадження.

Звичайно, новий Object () сьогодні також дуже швидкий, і Сміттєзбірники також дуже добре відновлюють короткочасні об’єкти.

Якщо ви не впевнені, що створення об'єкта буде дорогим, або вам потрібно зберегти деякий стан на основі потоку, вам краще піти на простіший розподіл, коли потрібно рішення, і лише переключившись на реалізацію ThreadLocal, коли Профайлер говорить вам, що вам потрібно.


4
+1 за те, що це єдина відповідь, яка насправді вирішує питання.
cletus 04.03.09

Чи можете ви дати мені приклад сучасної 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,55563 4,7128 4,5663 4,5617 4,5724 (середня = 4,6337 хв = 4,5509 макс = 13,9476)

Кількість ниток: 4 Всього тестів: 200

Тривалість роботи: (відображається останні 5) 2,3946 2,3979 2,33934 2,3937 2,3964 (середня = 2,5113 хв = 2,3884 макс. = 13,5496)

Кількість ниток: 8 Всього тестів: 200

Час роботи: (відображається останні 5) 2,44479 2,4362 2,4323 2,44472 2,4383 (середнє = 2,55562 хв = 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 4x двоядерний при тесті 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 та ці архітектури з кількістю процесорів.


5
+1 слава за те, що єдиним з них дає кількісні результати. Я трохи скептичний, тому що ці тести проводяться в Scala, але, як ви вже сказали, байт-коди Java повинні бути подібними ...
Гравітація

Дякую! Цей цикл while дає практично той самий байт-код, який створив би відповідний код Java. На різних віртуальних машинах можна спостерігати різні часи - проте це було перевірено на Sun JVM1.6.
axel22

Цей контрольний код не імітує належний варіант використання для 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-запущений зразок поля

Зразок поля з кінцем: 6044

0-запущений потік локальний зразок

Місцевий зразок кінцевої нитки: 6015

1-біговий зразок поля

Зразок поля з 1 кінця: 5095

1-запущений потік локальний зразок

Місцевий зразок 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 кінців: 5294

6-запущений потік локальний зразок

Місцевий зразок 6-кінцевої нитки: 5511

7-біговий зразок поля

Зразок поля з 7 кінців: 5119

7-запущений потік локальний зразок

Місцевий зразок 7-кінцевої нитки: 5793

8-біговий зразок поля

Зразок поля з 8 кінців: 4977

8-запущений потік локальний зразок

Місцевий зразок 8-кінцевої нитки: 6374

9-біговий зразок поля

Зразок поля з 9 кінців: 4841

9-запущений потік локальний зразок

Місцевий зразок 9-кінцевої нитки: 5471

Сер. Полів: 5051

ThreadLocal в середньому: 5664

Env:

версія openjdk "1.8.0_131"

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

Ubuntu 16.04 LTS


На жаль, це навіть близько до того, щоб бути дійсним тестом. А) Найбільша проблема: ви розподіляєте рядки з кожною ітерацією ( Int.toString)що надзвичайно дорого порівняно з тестуваним. Б), ви виконуєте дві операції карти на кожній ітерації, також абсолютно не пов'язані та дорогі. Спробуйте замість цього збільшити примітивний int з ThreadLocal. В) Використовуйте System.nanoTimeзамість System.currentTimeMillis, перший призначений для профілювання, другий - для цілей користувача та дати та може змінюватися під вашими ногами. Г) Вам слід повністю уникати розподілу, включаючи вищі рівні для ваших "прикладних" класів
Філіп Гуін,

3

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

Я був би дуже здивований, якщо побудова MessageDigest має якісь серйозні накладні витрати порівняно з фактичним його використанням.

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


0

Побудуйте його і виміряйте.

Крім того, вам потрібен лише один локальний потік, якщо ви інкапсулюєте свою поведінку перетравлення повідомлень в об’єкт. Якщо вам потрібні локальний MessageDigest та локальний байт [1000] для якоїсь мети, створіть об’єкт із полем messageDigest та байтом [] і помістіть цей об’єкт у ThreadLocal, а не обидва.


Спасибі, MessageDigest та байт [] по-різному використовуються, тому один об’єкт не потрібен.
Сармун 04.03.09
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.