Desempenho da variável ThreadLocal

86

Quanto é lido da ThreadLocalvariável mais lentamente do que do campo regular?

Mais concretamente, a criação de um objeto simples é mais rápida ou mais lenta do que o acesso à ThreadLocalvariável?

Presumo que seja rápido o suficiente para que ter uma ThreadLocal<MessageDigest>instância seja muito mais rápido do que criar uma instância de MessageDigestcada vez. Mas isso também se aplica a byte [10] ou byte [1000], por exemplo?

Edit: A pergunta é o que realmente está acontecendo quando a chamada ThreadLocalé obtida? Se fosse apenas um campo, como qualquer outro, a resposta seria "é sempre mais rápido", certo?

Sarmun
fonte
2
Um thread local é basicamente um campo que contém um hashmap e uma pesquisa onde a chave é o objeto de thread atual. Portanto, é muito mais lento, mas ainda assim rápido. :)
eckes
1
@eckes: certamente se comporta assim, mas geralmente não é implementado dessa forma. Em vez disso, Threads contêm um hashmap (não sincronizado) em que a chave é o ThreadLocalobjeto atual
sbk

Respostas:

40

A execução de benchmarks não publicados ThreadLocal.getleva cerca de 35 ciclos por iteração na minha máquina. Não muito. Na implementação da Sun, um mapa hash de teste linear personalizado em Threadmapas ThreadLocalde valores. Como só é acessado por um único thread, pode ser muito rápido.

A alocação de pequenos objetos leva um número semelhante de ciclos, embora, devido ao esgotamento do cache, você possa obter números um pouco mais baixos em um loop fechado.

A construção de MessageDigestprovavelmente será relativamente cara. Tem um bom estado de conservação e a construção passa pelo Providermecanismo do SPI. Você pode ser capaz de otimizar, por exemplo, clonando ou fornecendo oProvider .

Só porque pode ser mais rápido armazenar em cache em um ThreadLocal do que criar, não significa necessariamente que o desempenho do sistema aumentará. Você terá overheads adicionais relacionados ao GC, o que torna tudo mais lento.

A menos que seu aplicativo use muito, MessageDigestvocê pode querer considerar o uso de um cache seguro de thread convencional.

Tom Hawtin - tackline
fonte
5
IMHO, a maneira mais rápida é simplesmente ignorar o SPI e usar algo parecido new org.bouncycastle.crypto.digests.SHA1Digest(). Tenho certeza de que nenhum cache pode vencê-lo.
maaartinus,
57

Em 2009, algumas JVMs implementaram ThreadLocal usando um HashMap não sincronizado no objeto Thread.currentThread (). Isso o tornou extremamente rápido (embora não tão rápido quanto usar um acesso de campo regular, é claro), bem como garantiu que o objeto ThreadLocal fosse organizado quando o Thread morresse. Atualizando esta resposta em 2016, parece que a maioria (todas?) JVMs mais recentes usam um ThreadLocalMap com sondagem linear. Não tenho certeza sobre o desempenho deles - mas não posso imaginar que seja significativamente pior do que a implementação anterior.

Obviamente, new Object () também é muito rápido atualmente, e os Garbage Collectors também são muito bons em recuperar objetos de vida curta.

A menos que você tenha certeza de que a criação de objeto vai ser cara, ou você precisa persistir algum estado em uma base thread a thread, é melhor você ir para a solução mais simples de alocação quando necessário, e apenas alternar para uma implementação ThreadLocal quando um o profiler diz que você precisa.

Bill Michell
fonte
4
+1 por ser a única resposta a realmente responder à questão.
cletus
Você pode me dar um exemplo de uma JVM moderna que não usa análise linear para ThreadLocalMap? O Java 8 OpenJDK ainda parece estar usando ThreadLocalMap com sondagem linear. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/...
Karthick
1
@Karthick Desculpe, não posso. Eu escrevi isso em 2009. Vou atualizar.
Bill Michell
34

Boa pergunta, tenho me perguntado isso recentemente. Para lhe dar números definitivos, os benchmarks abaixo (em Scala, compilados virtualmente com os mesmos bytecodes do código Java equivalente):

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)
}

disponíveis aqui , foram realizados em um AMD 4x 2,8 GHz dual-core e um quad-core i7 com hyperthreading (2,67 GHz).

Estes são os números:

i7

Especificações: Intel i7 2x quad-core @ 2,67 GHz Teste: scala.threads.ParallelTests

Nome do teste: loop_heap_read

Número da linha: 1 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 9,0069 9,0036 9,0017 9,0084 9,0074 (média = 9,1034 min = 8,9986 máx = 21,0306)

Número da linha: 2 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 4,5563 4,7128 4,5663 4,5617 4,5724 (média = 4,6337 min = 4,5509 máx = 13,9476)

Número da linha: 4 Total de testes: 200

Tempos de execução: (mostrando os 5 últimos) 2,3946 2,3979 2,3934 2,3937 2,3964 (média = 2,5113 min = 2,3884 máx = 13,5496)

Número da linha: 8 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 2,4479 2,4362 2,4323 2,4472 2,4383 (média = 2,5562 min = 2,4166 máx = 10,3726)

Nome do teste: threadlocal

Número da linha: 1 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 91,1741 90,8978 90,6181 90,6200 90,6113 (média = 91,0291 min = 90,6000 max = 129,7501)

Número da linha: 2 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 45,3838 45,3858 45,6676 45,3772 45,3839 (média = 46,0555 min = 45,3726 máx = 90,7108)

Número da linha: 4 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 22,8118 22,8135 59,1753 22,8229 22,8172 (média = 23,9752 min = 22,7951 max = 59,1753)

Número da linha: 8 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 22,2965 22,2415 22,3438 22,3109 22,4460 (média = 23,2676 min = 22,2346 máx = 50,3583)

AMD

Especificações: AMD 8220 4x dual-core @ 2,8 GHz Teste: scala.threads.ParallelTests

Nome do teste: loop_heap_read

Trabalho total: 20000000 Número da linha: 1 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 12,625 12,631 12,634 12,632 12,628 (média = 12,7333 min = 12,619 máx = 26,698)

Nome do teste: loop_heap_read Trabalho total: 20000000

Tempos de execução: (mostrando os últimos 5) 6,412 6,424 6,408 6,397 6,43 (média = 6,5367 min = 6,393 máx = 19,716)

Número da linha: 4 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 3,385 4,298 9,7 6,535 3,385 (média = 5,6079 min = 3,354 máx = 21,603)

Número da linha: 8 Total de testes: 200

Tempos de execução: (mostrando os 5 últimos) 5,389 5,795 10,818 3,823 3,824 (média = 5,5810 min = 2,405 máx = 19,755)

Nome do teste: threadlocal

Número da linha: 1 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 200,217 207,335 200,241 207,342 200,23 (média = 202,2424 min = 200,184 máx = 245,369)

Número da linha: 2 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 100,208 100,199 100,211 103,781 100,215 (média = 102,2238 min = 100,192 máx = 129,505)

Número da linha: 4 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 62,101 67,629 62,087 52,021 55,766 (média = 65,6361 min = 50,282 máx = 167,433)

Número da linha: 8 Total de testes: 200

Tempos de execução: (mostrando os últimos 5) 40,672 74,301 34,434 41,549 28,119 (média = 54,7701 min = 28,119 máx = 94,424)

Resumo

Um thread local é cerca de 10 a 20 vezes maior que a leitura do heap. Ele também parece escalar bem nesta implementação de JVM e nessas arquiteturas com o número de processadores.

axel22
fonte
5
+1 Kudos por ser o único a dar resultados quantitativos. Estou um pouco cético porque esses testes são em Scala, mas como você disse, os bytecodes Java devem ser semelhantes ...
Gravidade
Obrigado! Este loop while resulta virtualmente no mesmo bytecode que o código Java correspondente produziria. No entanto, tempos diferentes podem ser observados em VMs diferentes - isso foi testado em um Sun JVM1.6.
axel22 01/09/11
Este código de benchmark não simula um bom caso de uso para ThreadLocal. No primeiro método: cada thread terá uma representação compartilhada na memória, a string não muda. No segundo método, você avalia o custo de uma pesquisa de hashtable onde a string é disjuntiva entre todos os threads.
Joelmob
A string não muda, mas é lida da memória (a gravação de "!"nunca ocorre) no primeiro método - o primeiro método é efetivamente equivalente a subclassificação Threade atribuição de um campo personalizado. O benchmark mede um caso extremo onde todo o cálculo consiste na leitura de uma variável / thread local - aplicativos reais podem não ser afetados dependendo de seu padrão de acesso, mas no pior caso, eles se comportarão como acima.
axel22,
4

Aqui vai outro teste. Os resultados mostram que ThreadLocal é um pouco mais lento do que um campo regular, mas na mesma ordem. Aprox 12% mais lento

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);
    }
}
}'

Resultado:

0-Amostra de campo de corrida

Amostra de campo 0-End: 6044

0-Running thread local sample

Amostra local de segmento 0-End: 6015

1-Amostra de campo de corrida

Amostra de campo de 1 extremidade: 5095

1 - Amostra local de thread em execução

Amostra local de thread de 1 extremidade: 5720

2-Amostra de campo de corrida

Amostra de campo de 2 extremidades: 4842

Amostra local de thread 2 em execução

Amostra local de 2 pontas: 5835

3-Amostra de campo de corrida

Amostra de campo de 3 extremidades: 4674

3-Running thread local sample

Amostra local de thread de 3 pontas: 5287

4-Amostra de campo de corrida

Amostra de campo de 4 extremidades: 4849

4-Executando amostra local de thread

Amostra local de 4 pontas: 5309

5-Amostra de campo de corrida

Amostra de campo de 5 extremidades: 4781

5-Amostra local de thread em execução

Amostra local de encadeamento de 5 extremidades: 5330

6-Amostra de campo de corrida

Amostra de campo 6-final: 5294

6-Running thread local sample

Amostra local do segmento 6-End: 5511

7-Amostra de campo de corrida

Amostra de campo de 7 pontas: 5119

7-Running thread local sample

Amostra local de thread 7-End: 5793

8-Amostra de campo de corrida

Amostra de campo de 8 pontas: 4977

8-Running thread local sample

Amostra local de segmento de 8 pontas: 6374

9-Amostra de campo de corrida

Amostra de campo 9-final: 4841

9-Running thread local sample

Amostra local de encadeamento de 9 extremidades: 5471

Média de campo: 5051

ThreadLocal avg: 5664

Env:

versão openjdk "1.8.0_131"

CPU Intel® Core ™ i7-7500U @ 2,70 GHz × 4

Ubuntu 16.04 LTS

Jpereira
fonte
Desculpe, isso não está nem perto de ser um teste válido. A) Maior problema: você está alocando Strings com cada iteração (o Int.toString)que é extremamente caro em comparação com o que você está testando. B) Você está fazendo duas operações de mapa a cada iteração, também totalmente não relacionadas e caras. Em vez disso, tente incrementar um int primitivo de ThreadLocal. C) Use em System.nanoTimevez de System.currentTimeMillis, o primeiro é para criação de perfil, o último é para fins de data e hora do usuário e pode mudar sob seus pés. D) Você deve evitar alocações totalmente, incluindo as de nível superior para suas classes "exemplo"
Philip Guin
3

@Pete é o teste correto antes de otimizar.

Eu ficaria muito surpreso se construir um MessageDigest tivesse alguma sobrecarga séria em comparação com usá-lo de forma ativa.

A falta de uso do ThreadLocal pode ser uma fonte de vazamentos e referências pendentes, que não têm um ciclo de vida claro, geralmente eu nunca uso o ThreadLocal sem um plano muito claro de quando um determinado recurso será removido.

Gareth Davis
fonte
0

Construa e meça.

Além disso, você só precisa de um threadlocal se encapsular seu comportamento de compilação de mensagem em um objeto. Se você precisar de um MessageDigest local e um byte local [1000] para algum propósito, crie um objeto com um messageDigest e um campo byte [] e coloque esse objeto no ThreadLocal em vez de ambos individualmente.

Pete Kirkham
fonte
Obrigado, MessageDigest e byte [] são usos diferentes, portanto, um objeto não é necessário.
Sarmun,