Este post continua a discussão sobre melhorias de desempenho que poderiam se tornar realidade se não fosse pelos diferentes mas. A parte anterior sobre o StringBuilder
está aqui .
Aqui, examinamos várias "melhorias" rejeitadas devido à falta de compreensão das sutilezas da especificação da linguagem, casos de canto não óbvios e outros motivos. Vamos lá!
Quando nada pressagia problemas
Acho que cada um de nós trabalhou com os métodos Collections.emptySet()
/ Collections.emptyList()
. Esses são métodos muito úteis que permitem retornar uma coleção imutável vazia sem criar um novo objeto. Olhando dentro da classe EmptyList
veremos isso:
private static class EmptyList<E> { public Iterator<E> iterator() { return emptyIterator(); } public Object[] toArray() { return new Object[0]; } }
Vê um forte potencial de melhoria? O método EmptyList.iterator()
retorna um iterador vazio da presença. Por que não fazer a mesma coisa com seus ouvidos para a matriz retornada pelo método toArray()
?
Em outras palavras, o método sempre deve retornar uma nova matriz.
Você dirá: "Ele é imutável! O que pode dar errado!?"
Somente especialistas experientes podem responder a esta pergunta:
- Quem é responsável?
- Especialistas responsáveis Paul Sandoz e Tagir Valeev
A resposta dos especialistashttp://mail.openjdk.java.net/pipermail/core-libs-dev/2017-September/049171.html
Observe também que isso altera o comportamento visível. E. g. alguém pode sincronizar no objeto de matriz retornado pela chamada toArray, portanto essa alteração pode causar o compartilhamento indesejado de bloqueios .
Uma vez sugeri uma melhoria semelhante: retornar EMPTY_LIST de Arrays.asList () quando a matriz fornecida tiver tamanho zero. Foi recusado pelo mesmo motivo [1].
http://mail.openjdk.java.net/pipermail/core-libs-dev/2015-September/035197.html
A propósito, provavelmente é razoável que Arrays.asList verifique o comprimento da matriz como:
public static <T> List<T> asList(T... a) { if(a.length == 0) return Collections.emptyList(); return new ArrayList<>(a); }
Parece razoável, certo? Por que criar uma nova lista para uma matriz vazia se você pode fazer uma lista pronta de graça?
Há uma razão para não fazer isso. No momento, Arrays.asList não especifica restrições à identidade da lista retornada. Adicionar a micro-otimização mudará isso. É um caso de ponta e um caso de uso questionável também, mas considerando que eu deixaria as coisas de maneira conservadora.
Esta afirmação provavelmente causará confusão:
E. g. alguém pode sincronizar no objeto de matriz retornado pela chamada toArray, portanto essa alteração pode causar o compartilhamento indesejado de bloqueios.
Você dirá: "Quem em sã consciência será sincronizado no array (!) Retornado (!!!) da coleção!?"
Não parece muito convincente, mas o idioma oferece essa oportunidade, o que significa que existe a possibilidade de um determinado usuário fazer isso (ou até já o fez). Em seguida, a alteração proposta alterará, na melhor das hipóteses, o comportamento do código e, na pior das hipóteses, levará a uma falha na sincronização (vá mais tarde, pegue) O risco é tão injustificado e o ganho esperado é tão insignificante que é melhor deixar tudo como está.
Em geral, a capacidade de sincronizar em qualquer objeto, kmk, foi o erro dos desenvolvedores de idiomas. Em primeiro lugar, o cabeçalho de cada objeto contém uma estrutura responsável pela sincronização e, em segundo lugar, nos encontramos na situação descrita acima, quando um objeto aparentemente imutável não pode ser retornado várias vezes, pois pode ser sincronizado com ele.
A moral dessa fábula é a seguinte: especificação e compatibilidade com versões anteriores são vacas sagradas de Java. Nem tente invadi-los: o guarda atira sem aviso.
Tentando, tentando ...
Existem várias classes baseadas em matriz no JDK de uma só vez e todas elas implementam os métodos List.indexOf()
e List.lastIndexOf()
:
- java.util.ArrayList
- java.util.Arrays $ ArrayList
- java.util.Vector
- java.util.concurrent.CopyOnWriteArrayList
O código desses métodos nessas classes é repetido quase um para um. Muitos aplicativos e estruturas também oferecem suas soluções para o mesmo problema:
Como resultado, temos um código perdido que precisa ser compilado (às vezes várias vezes), que ocorre no ReserverCodeCache, que precisa ser testado e que simplesmente vagueia de classe para classe sem quase nenhuma alteração.
Os desenvolvedores, por sua vez, gostam muito de escrever algo como
int i = Arrays.asList(array).indexOf(obj);
Gostaria de introduzir métodos de utilidade generalizados no JDK e usá-los em qualquer lugar, conforme sugerido . O patch é tão simples quanto dois centavos:
1) implementações de List.indexOf()
e List.lastIndexOf()
movem-se para java.util.Arrays
2) em vez disso, Arrays.indexOf()
e Arrays.lastIndexOf()
são chamados, respectivamente
Parece que o que poderia dar errado? O ganho dessa abordagem é [aparentemente] óbvio. Mas o artigo é sobre falhas, então pense no que pode dar errado.
- Quem é responsável?
- Especialistas responsáveis Martin Buchholz e Paul Sandoz
IMHO, um pouco tenso, mas mesmo assimMartin Buchholz:
Sergey, eu meio que mantenho todas essas classes de coleção e, ocasionalmente, também queria ter os métodos indexOf em Array.java. Mas:
Matrizes geralmente são desencorajadas. Quaisquer novos métodos estáticos em Arrays (ou, onde eu realmente os quero, no próprio objeto de matriz! Requer uma alteração na linguagem java!) Irá encontrar resistência.
Nós lamentamos o suporte a nulos em coleções, para que classes mais recentes, como ArrayDeque, não as suportem.
Outra variante que os usuários podem querer é que tipo de comparação de igualdade usar.
Nós lamentamos ter ArrayList com índice inicial baseado em zero - teria sido melhor ter o comportamento da matriz circular do ArrayDeque desde o primeiro dia.
O código para pesquisar fatias de matriz é muito pequeno, portanto você não está economizando muito. É fácil cometer um erro de um por um, mas isso também é verdade para uma API Arrays.
Paul Sandoz:
Eu não iria tão longe a ponto de dizer que as matrizes são desencorajadas, eu a giraria positivamente como "use com cuidado", pois são espinhosas, por exemplo, sempre mutáveis. Elas certamente poderiam ser melhoradas. Eu ficaria muito feliz em ver as matrizes implementarem uma matriz comum 'ish interface, poderemos progredir após os tipos de valor sedimentos.
Quaisquer novas adições ao Arrays seriam encontradas com alguma resistência, pelo menos por mim :-) Isso nunca adiciona apenas um ou dois métodos, muitos outros querem que o passeio também (todos os primitivos e variantes de intervalo). Portanto, qualquer novo recurso precisa ser suficientemente benéfico e, nesse caso, não acho que os benefícios sejam fortes o suficiente (como uma possível pressão no cache do código de redução).
Paul.
Correspondência: http://mail.openjdk.java.net/pipermail/core-libs-dev/2018-March/051968.html
A moral dessa fábula é a seguinte: seu pedaço engenhoso pode ser abatido para revisão simplesmente porque eles não terão nenhum valor especial nele. Bem, sim, há um código duplicado, mas isso não incomoda ninguém, então deixe-o vivo.
Melhorias para ArrayList? Eu tenho eles
Ciclomotor o patch não é meu, vou postá-lo para você pensar. A proposta em si foi apresentada aqui e parece muito atraente. Veja você mesmo:
A olho nu, a proposta é muito, muito lógica. Você pode medir o desempenho usando uma referência simples:
@State(Scope.Benchmark) public class ArrayListBenchmark { @State(Scope.Benchmark) public static class Data { @Param({"10", "100", "1000", "10000"}) public int size; ArrayList<Integer> arrayRandom = new ArrayList<Integer>(size); @Setup(Level.Invocation) public void initArrayList() { Random rand = new Random(); rand.setSeed(System.currentTimeMillis());
Resumo:
Benchmark (size) Mode Cnt Score Error Units construct_new_array_list 10 thrpt 25 388.212 ± 23.110 ops/s construct_new_array_list 100 thrpt 25 90.208 ± 7.995 ops/s construct_new_array_list 1000 thrpt 25 23.289 ± 1.687 ops/s construct_new_array_list 10000 thrpt 25 7.659 ± 0.560 ops/s construct_new_array_list 10 thrpt 25 562.678 ± 37.370 ops/s construct_new_array_list 100 thrpt 25 119.791 ± 13.232 ops/s construct_new_array_list 1000 thrpt 25 33.811 ± 3.812 ops/s construct_new_array_list 10000 thrpt 25 10.889 ± 0.564 ops/s
Nada mal para uma mudança tão simples. O principal é que parece não haver problema. Honestamente, crie uma matriz, copie honestamente os dados e não se esqueça do tamanho. Agora eles devem definitivamente aceitar o patch!
Mas lá estavaMartin Buchholz:
Não há dúvida de que podemos otimizar o caso de ArrayList -> ArrayList, mas e todas as outras implementações de Collection? ArrayDeque e CopyOnWriteArrayList vêm à mente.
ArrayList é uma classe popular a ser usada para fazer cópias de coleções. Onde você para?
Uma subclasse patológica de ArrayList pode decidir não armazenar elementos na matriz de backup, com a quebra resultante.
A solução abençoada para o problema de cópia de lista é provavelmente List.copyOf https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html#copyOf(java .util.Collection ), que pode fazer a otimização que você espera.
Alan Bateman
ArrayList não é final; portanto, é possível que alguém o tenha estendido para usar algo diferente de elementData. Pode ser mais seguro usar a identidade da classe do que instanceof.
Nada me proíbe desassociar de ArrayList
e armazenar dados em uma lista vinculada. Então, uma c instanceof ArrayList
retornará a verdade, chegaremos à área de cópia e cairemos com segurança.
A moral dessa fábula é a seguinte: uma possível mudança de comportamento pode ser a causa do fracasso. Em outras palavras, é preciso ter em mente a probabilidade até da mudança mais absurda, se for permitida pelos meios da linguagem. E sim, poderia ter dado certo se ArrayList
tivesse declarado final
desde o início.
Especificação novamente
Ao depurar meu aplicativo, acidentalmente caí nas entranhas do Spring e encontrei o seguinte código :
Felizmente, entrando em java.lang.reflect.Constructor.getParameterTypes()
rolei o código um pouco mais baixo e achei um lindo:
@Override public Class<?>[] getParameterTypes() { return parameterTypes.clone(); } public int getParameterCount() { return parameterTypes.length; }
Você vê sim? Se precisarmos descobrir o número de argumentos do construtor / método, chame java.lang.reflect.Method.getParameterCount()
e faça isso sem copiar a matriz. Verifique se o jogo vale a pena no caso mais simples, no qual o método não possui parâmetros:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MethodToStringBenchmark { private Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("toString"); } @Benchmark public int getParameterCount() { return method.getParameterCount(); } @Benchmark public int getParameterTypes() { return method.getParameterTypes().length; } }
Na minha máquina e com o JDK 11, acontece assim:
Benchmark Mode Cnt Score Error Units getParameterCount avgt 25 2,528 ± 0,085 ns/op getParameterCount:·gc.alloc.rate avgt 25 ≈ 10⁻⁴ MB/sec getParameterCount:·gc.alloc.rate.norm avgt 25 ≈ 10⁻⁷ B/op getParameterCount:·gc.count avgt 25 ≈ 0 counts getParameterTypes avgt 25 7,299 ± 0,410 ns/op getParameterTypes:·gc.alloc.rate avgt 25 1999,454 ± 89,929 MB/sec getParameterTypes:·gc.alloc.rate.norm avgt 25 16,000 ± 0,001 B/op getParameterTypes:·gc.churn.G1_Eden_Space avgt 25 2003,360 ± 91,537 MB/sec getParameterTypes:·gc.churn.G1_Eden_Space.norm avgt 25 16,030 ± 0,045 B/op getParameterTypes:·gc.churn.G1_Old_Gen avgt 25 0,004 ± 0,001 MB/sec getParameterTypes:·gc.churn.G1_Old_Gen.norm avgt 25 ≈ 10⁻⁵ B/op getParameterTypes:·gc.count avgt 25 2380,000 counts getParameterTypes:·gc.time avgt 25 1325,000 ms
O que podemos fazer sobre isso? Podemos procurar o uso do antipadrão Method.getParameterTypes().length
no JDK (pelo menos em java.base
) e substituí-lo onde for necessário:
java.lang.invoke.MethodHandleProxies

java.util.concurrent.ForkJoinTask

java.lang.reflect.Executable

sun.reflect.annotation.AnnotationType

O patch foi enviado junto com uma carta de apresentação .
De repente, verificou-se que durante vários anos houve uma tarefa semelhante, e até mudanças foram preparadas para isso. Os comentários observaram um aumento de desempenho bastante decente para essas mudanças simples. No entanto, eles e meu adesivo estão limpos até agora e ficam imóveis. Porque Provavelmente porque os desenvolvedores estão ocupados demais com coisas mais necessárias e estupidamente não colocam as mãos nelas.
A moral desta fábula é a seguinte: suas mudanças engenhosas podem congelar simplesmente devido à falta de trabalhadores.
Mas este não é o fim! Durante a discussão sobre a racionalidade da substituição descrita em outros projetos, camaradas mais experientes apresentaram uma contraproposta: talvez você não deva fazer a substituição de Method.getParameterTypes().length -> Method.getParameterCount()
com suas mãos, mas confiar isso ao compilador? Isso é possível e será "legal"?
Vamos tentar verificar usando o teste:
@Test void arrayClone() { final Object[] objects = new Object[3]; objects[0] = "azaza"; objects[1] = 365; objects[2] = 9876L; final Object[] clone = objects.clone(); assertEquals(objects.length, clone.length); assertSame(objects[0], clone[0]); assertSame(objects[1], clone[1]); assertSame(objects[2], clone[2]); }
que passa e mostra que, se a matriz clonada não sair do escopo, ela poderá ser excluída porque o acesso a qualquer elemento de suas células ou ao campo de length
pode ser obtido a partir do original.
O JDK pode fazer isso? Verificamos:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class ArrayAllocationEliminationBenchmark { private int length = 10; @Benchmark public int baseline() { return new int[length].length; } @Benchmark public int baselineClone() { return new int[length].clone().length; } }
Saída para JDK 13:
Benchmark Mode Cnt Score Error Units baseline avgt 50 6,135 ± 0,140 ns/op baseline:·gc.alloc.rate.norm avgt 50 56,000 ± 0,001 B/op clone avgt 50 18,359 ± 0,619 ns/op clone:·gc.alloc.rate.norm avgt 50 112,000 ± 0,001 B/op
Acontece que o openjdk não sabe como lançar um new int[length]
, diferente do Graal , hehe:
Benchmark Mode Cnt Score Error Units baseline avgt 25 2,470 ± 0,156 ns/op baseline:·gc.alloc.rate.norm avgt 25 0,005 ± 0,008 B/op lone avgt 25 13,086 ± 1,059 ns/op lone:·gc.alloc.rate.norm avgt 25 112,113 ± 0,115 B/op
Acontece que você pode ajustar um pouco o compilador de otimização do openjdk para que ele possa fazer o que o Graal pode fazer. Como nem todos podem entrar em um anúncio positivo no código da VM e registrar algo significativo, eu me limitei a uma carta na qual eu declarava minhas observações.
Acabou, e existem várias sutilezas. Vladimir Ivanov indica que:
Considerando que não há como aumentar / diminuir as matrizes Java,
A transformação "cloned_array.length => original_array.length" está correta
independentemente de a variante clonada escapar ou não.
Além disso, a transformação já está lá:
http://hg.openjdk.java.net/jdk/jdk/file/tip/src/hotspot/share/opto/memnode.cpp#l2388
Não analisei os benchmarks que você mencionou, mas parece que
O acesso cloned_array.length não é a razão pela qual a matriz clonada ainda é
lá.
Em relação às suas outras idéias, redirecione os acessos da instância clonada para
original é problemático (no caso geral), pois o compilador precisa provar
não houve alterações nas duas versões e os acessos indexados tornam ainda mais
mais difícil. E os pontos seguros também causam problemas (para rematerialização).
Mas concordo que seria bom cobrir (pelo menos) casos simples de
cópia defensiva.
Ou seja, bater em um clone parece ser possível, e não particularmente difícil. Mas com a conversão
int len = new int[arrayLength].length;
->
int len = arrayLength;
dificuldades surgem:
Não eliminamos alocações de matrizes que não possuem um comprimento conhecido
porque eles podem causar uma exceção NegativeArraySize. Nesse caso, nós
deve ser capaz de provar que o comprimento é positivo.
Enfim - Eu tenho um patch quase pronto que substitui o array não utilizado
alocações com uma guarda adequada.
Em outras palavras, você não pode simplesmente tirar e jogar fora a criação de uma matriz, porque, de acordo com a especificação, a execução deve gerar uma NegativeArraySizeException
e não há nada que possamos fazer sobre isso:
@Test void arrayWithNwgativeSize() { int length = 0; try { int newLen = -3; length = new Object[newLen].length;
Por que o Graal foi capaz? Acho que o motivo é que o valor do campo length
no benchmark acima era constante e sempre igual a 10, o que permitiu ao criador de perfil concluir que a verificação de um valor negativo não é necessária, o que significa que ele pode ser removido junto com a criação do próprio array. Corrija nos comentários se eu cometi um erro.
Isso é tudo por hoje :) Adicione seus exemplos nos comentários, vamos entender.