Cliff Click é o CTO da Cratus (sensores de IoT para melhoria de processos), o fundador e co-fundador de várias startups (incluindo Rocket Realtime School, Neurensic e H2O.ai) com várias saídas bem-sucedidas. Cliff escreveu seu primeiro compilador aos 15 anos (Pascal para TRS Z-80)! Mais conhecido por trabalhar em C2 em Java (IR do Sea of Nodes). Esse compilador mostrou ao mundo que o JIT pode produzir código de alta qualidade, que se tornou um dos fatores para tornar o Java uma das principais plataformas de software modernas. Cliff então ajudou a Azul Systems a construir um mainframe de 864 núcleos com software Java puro que suportava pausas de GC em um heap de 500 gigabytes por 10 milissegundos. Em geral, Cliff conseguiu trabalhar em todos os aspectos da JVM.
Este hubrapost é uma ótima entrevista com Cliff. Falaremos sobre os seguintes tópicos:
- Transição para otimizações de baixo nível
- Como fazer muita refatoração
- Modelo de custo
- Treinamento de otimização de baixo nível
- Estudos de caso de melhoria da produtividade
- Por que criar sua própria linguagem de programação
- Carreira de engenheiro de desempenho
- Desafios técnicos
- Um pouco sobre alocação de registros e multicore
- O maior desafio da vida
Entrevistas realizadas por:
- Andrey Satarin, da Amazon Web Services. Em sua carreira, ele conseguiu trabalhar em projetos completamente diferentes: ele testou o banco de dados NewSQL distribuído no Yandex, o sistema de detecção de nuvens da Kaspersky Lab, o jogo multiplayer no Mail.ru e o serviço de cálculo de câmbio no Deutsche Bank. Ele está interessado em testar sistemas de back-end e distribuídos em larga escala.
- Vladimir Sitnikov, do Netcracker. Por dez anos, ele trabalha no desempenho e na escalabilidade do NetCracker OS, um software usado pelas operadoras de telecomunicações para automatizar os processos de gerenciamento de equipamentos de rede e de rede. Ele está interessado em questões de desempenho do Java e Oracle Database. O autor de mais de uma dúzia de melhorias de desempenho no driver oficial do PostgreSQL JDBC.
Transição para otimizações de baixo nível
Andrei : Você é uma pessoa famosa no mundo da compilação JIT, em Java e trabalha com desempenho em geral, certo?
Cliff : É isso aí!
Andrew : Vamos começar com perguntas gerais sobre como trabalhar no desempenho. O que você acha da escolha entre otimizações de alto e baixo nível, como o trabalho no nível da CPU?
Cliff : É fácil. O código mais rápido é aquele que nunca é executado. Portanto, você sempre precisa começar de alto nível, trabalhar com algoritmos. Uma melhor notação O superará uma pior notação O, a menos que algumas constantes razoavelmente grandes interfiram. As coisas de baixo nível são as mais recentes. Normalmente, se você otimizou o restante da pilha o suficiente, e ainda resta algo interessante - esse é o nível baixo. Mas como começar de alto nível? Como descobrir que trabalho suficiente foi feito em alto nível? Bem ... de jeito nenhum. Não há receitas prontas. Você precisa entender o problema, decidir o que fará (para não executar etapas desnecessárias no futuro) e, em seguida, poderá descobrir um criador de perfil que possa dizer algo útil. Em algum momento, você mesmo entende que se livrou de coisas desnecessárias e é hora de ajustar o nível mais baixo. Este é definitivamente um tipo especial de arte. Muitas pessoas fazem coisas desnecessárias, mas se movem tão rápido que não têm tempo para se preocupar com o desempenho. Mas isso é contanto que a questão não fique de pé. Normalmente, 99% das vezes ninguém se interessa pelo que faço, até o momento em que algo importante com o qual alguém se importa não entra no caminho crítico. E aqui todo mundo começa a incomodá-lo com o tópico "por que não funcionou perfeitamente desde o início". Em geral, sempre há algo a melhorar no desempenho. Mas 99% das vezes você não tem leads! Você está apenas tentando conseguir algo para funcionar e, no processo, entende o que é importante. Você nunca pode saber antecipadamente que esta peça precisa ser perfeita, portanto, em essência, você precisa ser perfeita em tudo. E isso é impossível, e você não faz isso. Sempre há um monte de coisas para corrigir - e isso é perfeitamente normal.
Como fazer muita refatoração
Andrew : Como você trabalha no desempenho? Esta é uma questão transversal. Por exemplo, você teve que trabalhar em problemas decorrentes da interseção de uma grande quantidade de funcionalidades existentes?
Cliff : Eu tento evitar isso. Se eu sei que o desempenho se tornará um problema, penso nisso antes de começar a codificar, especialmente em estruturas de dados. Mas muitas vezes você descobre tudo isso muito mais tarde. E então você deve tomar medidas extremas e fazer o que chamo de "reescrever e conquistar": você precisa se agarrar a uma peça bastante grande. Parte do código ainda precisará ser reescrita devido a problemas de desempenho ou outra coisa. Qualquer que seja o motivo para reescrever o código, é quase sempre melhor reescrever um pedaço maior do que um pedaço menor. Nesse momento, todos começam a tremer de medo: "Oh meu Deus, você não pode tocar tanto código!" Mas, de fato, essa abordagem quase sempre funciona muito melhor. Você precisa enfrentar imediatamente o grande problema, desenhar um grande círculo ao redor e dizer: vou reescrever tudo dentro do círculo. A borda é muito menor que o conteúdo dentro dela que precisa ser substituído. E se esse delineamento de bordas permitir que você faça o trabalho perfeitamente - você tem suas mãos desatadas, faça o que quiser. Depois de entender o problema, o processo de reescrita é muito mais fácil, então morda um pedaço grande!
Ao mesmo tempo, quando você reescreve em grandes blocos e entende que o desempenho se tornará um problema, você pode imediatamente começar a se preocupar com isso. Geralmente, isso se transforma em coisas simples como "não copie dados, gerencie os dados da maneira mais simples possível, diminua-os". Em reescritas grandes, existem maneiras padrão de melhorar o desempenho. E eles quase sempre giram em torno de dados.
Modelo de custo
Andrew : Em um dos podcasts, você falou sobre modelos de custo no contexto da produtividade. Você pode explicar o que isso significava?
Cliff : Claro. Nasci em uma época em que o desempenho do processador era extremamente importante. E esta era está voltando novamente - o destino não é isento de ironia. Comecei a viver nos dias de máquinas de oito bits; meu primeiro computador trabalhava com 256 bytes. São bytes. Tudo era muito pequeno. Tivemos que ler as instruções e, assim que começamos a subir a pilha de linguagens de programação, as linguagens assumiram cada vez mais. Havia Assembler, depois Basic, C e C assumiram o trabalho com muitos detalhes, como alocação de registro e seleção de instruções. Mas tudo estava bem claro lá, e se eu fiz um ponteiro para uma instância de uma variável, receberei carga, e o custo é conhecido por esta instrução. O ferro produz um número conhecido de ciclos de máquina, para que a velocidade de execução de diferentes peças possa ser calculada simplesmente adicionando todas as instruções que você estava prestes a executar. Cada comparação / teste / filial / chamada / carga / loja pode ser dobrada e dizer: aqui você tem o prazo de entrega. Ao melhorar o desempenho, você definitivamente prestará atenção em que tipo de números corresponde a pequenos ciclos quentes.
Mas assim que você muda para Java, Python e coisas semelhantes, você se afasta rapidamente do ferro de baixo nível. Quanto custa uma chamada getter em Java? Se o JIT no HotSpot estiver alinhado corretamente, ele será carregado, mas, se não, será uma chamada de função. Como o desafio está no hot loop, ele desfará todas as outras otimizações nesse loop. Portanto, o valor real será muito maior. E você perde imediatamente a capacidade de examinar um pedaço de código e entender que devemos executá-lo em termos de velocidade do clock do processador, memória usada e cache. Tudo isso se torna interessante apenas se você realmente ficar bêbado com o desempenho.
Agora, estamos em uma situação em que a velocidade dos processadores quase não cresce há uma década. Os velhos tempos estão de volta! Você não pode mais contar com um bom desempenho de thread único. Mas se você de repente se envolver em computação paralela - é incrivelmente difícil, todo mundo olha para você como James Bond. A aceleração dez vezes maior aqui geralmente ocorre naqueles lugares onde alguém dá um tapa em algo. A simultaneidade requer muito trabalho. Para obter a mesma aceleração dez vezes maior, você precisa entender o modelo de custo. Quanto e quanto custa. E para isso, você precisa entender como a língua se apoia no ferro subjacente.
Martin Thompson tem uma ótima palavra para seu blog Mechanical Sympathy ! Você precisa entender o que o ferro fará, como exatamente ele fará e por que geralmente faz o que faz. Com isso, é bastante simples começar a ler as instruções e descobrir onde o tempo de execução está fluindo. Se você não possui o treinamento adequado, está apenas procurando um gato preto em um quarto escuro. Vejo constantemente pessoas otimizando o desempenho que não têm idéia do que diabos estão fazendo. Eles são muito atormentados e realmente não vão a algum lugar. E quando pego o mesmo pedaço de código, coloco alguns pequenos hacks lá e acelero cinco ou dez vezes, eles são assim: bem, é tão desonesto que já sabíamos que você é melhor. Isso é incrível. Do que estou falando ... o modelo de custo é sobre o código que você escreve e a velocidade com que ele funciona em média na imagem geral.
Andrew : E como manter esse volume em sua cabeça? Isso é alcançado com mais experiência ou? Onde essa experiência é adquirida?
Cliff : Bem, minha experiência não foi a maneira mais fácil. Programei no Assembler em um momento em que era possível entender cada instrução individual. Parece bobagem, mas desde então, na minha cabeça, na minha memória, o conjunto de instruções do Z80 permanece para sempre. Não lembro os nomes das pessoas nem um minuto após a conversa, mas lembro do código escrito há 40 anos. Engraçado, parece uma síndrome de " idiota aprendido ".
Treinamento de otimização de baixo nível
Andrew : Existe alguma maneira mais simples de entrar nos negócios?
Cliff : Sim e não. O ferro que todos nós usamos não mudou muito durante esse período. Todo mundo usa x86, com exceção dos smartphones Arm. Se você não faz uma incorporação hardcore, você tem a mesma coisa. Ok, depois. As instruções também não mudaram por séculos. Você precisa escrever algo no Assembler. Um pouco, mas o suficiente para começar a entender. Você está sorrindo, mas estou falando sério. É necessário entender a correspondência da linguagem e do ferro. Depois disso, você precisa ir, fazer xixi um pouco e fazer um pequeno compilador de brinquedos para uma linguagem de brinquedo pequeno. "Brinquedo" significa que você precisa fazê-lo em um período de tempo razoável. Pode ser super simples, mas deve gerar instruções. O ato de gerar instruções nos permitirá entender o modelo de custo da ponte entre o código de alto nível no qual todos escrevem e o código de máquina que roda no hardware. Essa correspondência será queimada no cérebro no momento da escrita do compilador. Até o compilador mais simples. Depois disso, você pode começar a olhar para Java e o fato de que ela possui uma lacuna semântica mais profunda, e construir pontes sobre ela é muito mais difícil. Em Java, é muito mais difícil entender se nossa ponte acabou sendo boa ou ruim, o que a fará desmoronar e não. Mas você precisa de algum ponto de partida quando olhar para o código e entender: "Sim, esse getter deve alinhar sempre". E acontece que às vezes isso acontece, com exceção da situação em que o método fica muito grande e o JIT começa a alinhar tudo. O desempenho desses locais pode ser previsto instantaneamente. Normalmente, os getters funcionam bem, mas então você olha para os grandes loops quentes e percebe que há algum tipo de chamada de função flutuando que não sabe o que está fazendo. Esse é o problema com o uso generalizado de getters, a razão pela qual eles não estão alinhados - não está claro se é um getter. Se você tem uma base de código super pequena, basta lembrar e dizer: isto é um getter, mas este é um setter. Em uma grande base de código, cada função vive sua própria história, que, em geral, não é conhecida por ninguém. O criador de perfil diz que perdemos 24% do nosso tempo em algum tipo de ciclo e, para entender o que esse ciclo faz, precisamos examinar cada função interna. É impossível entender isso sem estudar a função, e isso atrasa seriamente o processo de compreensão. Por isso que não uso getters e setters, fui para um novo nível!
Onde obter o modelo de custo? Bem, você pode ler algo, é claro ... Mas acho que a melhor maneira é agir. Faça um pequeno compilador e essa será a melhor maneira de realizar o modelo de custo e ajustá-lo em sua própria cabeça. Um pequeno compilador que funcionaria na programação de microondas é uma tarefa para iniciantes. Bem, quero dizer, se você já tem habilidades de programação, elas devem ser suficientes. Todas essas coisas são como analisar uma string, que você terá algum tipo de expressão algébrica, retire as instruções de operações matemáticas de lá na ordem correta, pegue os valores corretos dos registros - tudo isso é feito de uma só vez. E enquanto você fizer isso, ele será impresso no cérebro. Acho que todo mundo sabe o que o compilador faz. E isso dará uma compreensão do modelo de custo.
Estudos de caso de melhoria da produtividade
Andrew : Em que mais vale a pena prestar atenção ao trabalhar no desempenho?
Penhasco : Estruturas de Dados. A propósito, sim, eu não leciono essas aulas há muito tempo ... Rocket School . Foi engraçado, mas foi preciso muito esforço para investir, e eu também tenho vida! Ok. Portanto, em uma das grandes e interessantes aulas "Onde está o seu desempenho", dei aos alunos um exemplo: dois gigabytes e meio de dados fintech foram lidos em um arquivo CSV e tivemos que calcular o número de produtos vendidos. Dados regulares do mercado de ticks. Pacotes UDP convertidos em formato de texto desde os anos 70. O Chicago Mercantile Exchange é todo tipo de coisas como manteiga, milho, soja e coisas do gênero. Era necessário contar esses produtos, o número de transações, o volume médio de movimentação de fundos e mercadorias, etc. Essa é uma matemática de negociação bastante simples: encontre o código do produto (são de um a dois caracteres na tabela de hash), obtenha a quantia, adicione-a a um dos conjuntos de transações, adicione volume, agregue valor e algumas outras coisas. Matemática muito simples. A implementação do brinquedo foi bem direta: tudo está no arquivo, eu o leio e o movo, separando as entradas individuais nas seqüências Java, procurando as coisas necessárias nelas e dobrando-as de acordo com a matemática descrita acima. E funciona a uma velocidade baixa.
Com essa abordagem, tudo fica óbvio o que está acontecendo, e a computação paralela não ajudará aqui, certo? Acontece que um aumento de produtividade em cinco vezes pode ser alcançado apenas escolhendo as estruturas de dados corretas. E isso surpreende até programadores experientes! No meu caso particular, o truque era que você não deveria fazer alocações de memória em um hot loop. Bem, essa não é a verdade, mas em geral - você não deve destacar "uma vez em X" quando X for grande o suficiente. Quando X tem dois gigabytes e meio, você não deve alocar nada "uma vez por letra", "uma vez por linha" ou "uma vez por campo", nada disso. Isso é exatamente o que leva tempo. Como isso funciona? Imagine fazer uma chamada para String.split()
ou BufferedReader.readLine()
. Readline
uma linha a partir de um conjunto de bytes vindos da rede, uma vez para cada linha, para cada uma das centenas de milhões de linhas. Eu pego essa linha, analiso e jogo fora. Por que jogá-lo fora - bem, eu já o processei, isso é tudo. Portanto, para cada byte lido a partir desses 2.7G, dois caracteres serão escritos na linha, ou seja, 5.4G já, e eu não preciso mais deles, portanto eles serão descartados. Se você observar a largura de banda da memória, carregamos 2.7G, que passam pela memória e pelo barramento de memória no processador e, em seguida, o dobro é enviado para a linha que está na memória, e tudo isso se desfaz quando cada nova linha é criada. Mas preciso ler, o ferro lê, mesmo que tudo seja esfregado. E tenho que anotá-la, porque criei a linha e os caches estavam cheios - o cache não pode caber 2.7G. No total, para cada byte lido, leio mais dois bytes e escrevo dois bytes adicionais e, como resultado, eles têm uma proporção de 4: 1 - nessa proporção, perdemos a largura de banda da memória. E acontece que, se eu fizer String.split()
, não faço isso da última vez, pode haver outros 6-7 campos dentro. Portanto, o código de leitura clássico de CSV seguido pela análise de linha leva a uma perda de largura de banda de memória na região de 14: 1 em relação ao que você realmente gostaria de ter. Se você jogar fora essas secreções, poderá obter uma aceleração de cinco vezes.
E não é tão difícil assim. Se você olhar o código do ângulo certo, tudo se tornará bastante simples, assim que você perceber a essência do problema. Nem pare de alocar memória: o único problema é que você aloca algo e ele morre imediatamente e queima um recurso importante ao longo do caminho, que neste caso é a largura de banda da memória. E tudo isso resulta em uma queda na produtividade. No x86, você geralmente precisa gravar ativamente os relógios do processador e aqui você queimou toda a memória muito antes. Solução - você precisa reduzir a quantidade de descarga.
Outra parte do problema é que, se você inicia o criador de perfil quando a faixa de memória termina, exatamente no momento em que isso acontece, geralmente espera que o cache retorne, porque está cheio de lixo que você acabou de gerar com todas essas linhas. Portanto, cada operação de carregamento ou armazenamento fica lenta, porque leva a falhas no cache - o cache inteiro fica lento, aguardando a saída do lixo. Portanto, o criador de perfil mostrará apenas ruídos aleatórios quentes manchados ao longo de todo o ciclo - não haverá instruções quentes ou locais separados no código. Apenas o barulho. E se você observar os ciclos do GC, todos serão de geração jovem e super rápidos - microssegundos ou milissegundos no máximo. Afinal, toda essa memória morre instantaneamente. Você aloca bilhões de gigabytes e os corta, corta e corta novamente. Tudo isso acontece muito rapidamente. Acontece que existem ciclos de GC baratos, ruído quente ao longo de todo o ciclo, mas queremos obter uma aceleração de 5x. Naquele momento, algo deveria se fechar na minha cabeça e soar: "por que?" O excesso de largura de banda não aparece no depurador clássico; você precisa executar o depurador do contador de desempenho de hardware e vê-lo você mesmo e diretamente. E não diretamente, pode-se suspeitar desses três sintomas. O terceiro sintoma é quando você olha para o que destaca, pergunta ao criador de perfil e ele responde: "Você fez um bilhão de linhas, mas o GC trabalhou de graça". Assim que isso aconteceu, você percebe que gerou muitos objetos e queimou toda a faixa de memória. Existe uma maneira de descobrir isso, mas não é óbvio.
O problema está na estrutura de dados: a estrutura básica por trás de tudo o que acontece, é muito grande, é 2,7G no disco, portanto, fazer uma cópia dessa coisa é muito indesejável - eu quero carregá-lo do buffer de byte da rede imediatamente nos registradores para não ler / gravar na string e para trás cinco vezes. Infelizmente, o Java por padrão não fornece essa biblioteca como parte do JDK. Mas isso é trivial, certo? De fato, essas são de 5 a 10 linhas de código que serão usadas para implementar seu próprio carregador de linhas em buffer, que repete o comportamento da classe de linha, enquanto é um invólucro em torno do buffer de bytes subjacente. Como resultado, acontece que você trabalha quase como se estivesse com seqüências de caracteres, mas, de fato, existem ponteiros movendo-se para o buffer, e bytes brutos não são copiados em nenhum lugar e, portanto, os mesmos buffers são reutilizados, uma e outra vez, e o sistema operacional fica feliz em assumir coisas para as quais ele se destina, como buffer duplo oculto desses buffers de bytes, e você mesmo não processa mais um fluxo interminável de dados desnecessários. A propósito, você entende que, ao trabalhar com o GC, é garantido que cada alocação de memória não estará visível para o processador após o último ciclo do GC? Portanto, tudo isso não pode estar no cache e, em seguida, ocorre uma falha 100% garantida. Ao trabalhar com um ponteiro em x86, subtrair um registro da memória leva de 1 a 2 ciclos e, assim que isso acontece, você paga, paga, paga, porque a memória está em Nove caches - e esse é o custo de alocar memória. Valor presente.
Em outras palavras, as estruturas de dados são as mais difíceis de mudar. E assim que você perceber que escolheu a estrutura de dados incorreta que reduzirá a produtividade no futuro, geralmente precisará aumentar o trabalho essencial, mas se não o fizer, será pior. Primeiro de tudo, você precisa pensar em estruturas de dados, isso é importante. O principal custo aqui está nas estruturas de dados em negrito que elas estão começando a usar no estilo "Copiei a estrutura de dados X na estrutura de dados Y, porque gosto mais da forma". Mas a operação de cópia (que parece barata) na verdade gasta uma faixa de memória e aqui todo o tempo de execução perdido é enterrado. Se eu tenho uma string gigante com JSON e quero transformá-la em uma árvore DOM estruturada do POJO ou algo assim, a operação de analisar essa string e criar um POJO e, em seguida, uma nova chamada para o POJO no futuro se tornará inútil - não é algo barato. Exceto se você for executado no POJO com muito mais frequência do que em uma linha. Ao invés disso, você pode tentar descriptografar a string e retirar apenas o que precisa, sem transformá-la em nenhum POJO. Se tudo isso acontecer no caminho a partir do qual o desempenho máximo é necessário, não há POJOs para você - você precisa de alguma forma cavar diretamente na linha.
Por que criar sua própria linguagem de programação
Andrei : Você disse que, para entender o modelo de custo, você precisa escrever sua própria linguagem pequena ...
Cliff : Não é um idioma, mas um compilador. Linguagem e compilador são duas coisas diferentes. A diferença mais importante está na sua cabeça.
Andrei : A propósito, até onde eu sei, você está experimentando criar seus próprios idiomas. Porque
Cliff : Porque eu posso! Estou meio aposentado, então este é o meu hobby. Eu tenho implementado os idiomas de outra pessoa a vida toda. Eu também trabalhei duro no estilo de codificação. E também porque vejo problemas em outros idiomas. Vejo que existem maneiras melhores de fazer as coisas habituais. E eu os usaria. Eu apenas me cansei de ver problemas em mim mesmo, em Java, em Python, em qualquer outra linguagem. Estou escrevendo sobre React Native, JavaScript e Elm como um hobby, que não é sobre aposentadoria, mas sobre trabalho ativo. E também escrevo em Python e, provavelmente, continuarei trabalhando no aprendizado de máquina para back-ends Java. Existem muitos idiomas populares e todos eles têm recursos interessantes. Todo mundo é bom em algo próprio e você pode tentar juntar todos esses chips. Então, estudo as coisas que são interessantes para mim, o comportamento da linguagem, tento criar uma semântica razoável. E até agora estou fazendo isso! No momento, estou lutando com a semântica da memória, porque quero tê-la em C e Java e obter um modelo de memória forte e semântica de memória para cargas e lojas. Ao mesmo tempo, tenha inferência de tipo automática como em Haskell. Aqui, estou tentando misturar inferência do tipo Haskell com memória trabalhando em C e Java. Eu venho fazendo isso nos últimos 2-3 meses, por exemplo.
Andrei : Se você está construindo uma linguagem que tem aspectos melhores de outras línguas, você pensou que alguém faria o oposto: pegue suas idéias e use-as?
Cliff : É assim que novos idiomas aparecem! Por que o Java é semelhante ao C? Como C tinha uma boa sintaxe que todos entendiam e o Java foi inspirado por essa sintaxe, adicionando segurança de tipo, verificando os limites de matrizes, GC, e eles melhoraram algumas coisas de C. Eles adicionaram os seus. Mas eles foram inspirados um pouco, certo? Todo mundo fica sobre os ombros dos gigantes que vieram antes de você - é assim que o progresso é feito.
Andrew : Pelo que entendi, seu idioma estará seguro em relação ao uso de memória. Você já pensou em implementar algo como um verificador de empréstimo da Rust? Você olhou para ele, como ele gostou de você?
Cliff : Bem, eu tenho escrito C há muito tempo, com todos esses malloc e gratuitos, e gerencio manualmente a vida toda. Você sabe, 90-95% de um tempo de vida gerenciado manualmente tem a mesma estrutura. E é muito, muito doloroso fazer isso manualmente. Eu gostaria que o compilador simplesmente dissesse o que está acontecendo lá e o que você conseguiu com suas ações. Para algumas coisas, um verificador de empréstimo faz isso imediatamente. E ele deve exibir informações automaticamente, entender tudo e nem me sobrecarregar para afirmar esse entendimento. Ele deve fazer pelo menos uma análise de escape local e, apenas se não for bem-sucedido, você precisará adicionar anotações de tipo que descrevam o tempo de vida útil - e esse esquema é muito mais complicado do que um verificador de empréstimos ou qualquer verificador de memória existente. A escolha entre "tudo está em ordem" e "eu não entendi nada" - não, deve haver algo melhor.
Portanto, como uma pessoa que escreveu muito código C, acho que ter o suporte ao controle automático da vida útil é a coisa mais importante. E me cansei de quanto Java usa memória e a principal reclamação está no GC. Ao alocar memória em Java, você não retornará a memória local no último loop do GC. Em idiomas com gerenciamento de memória mais preciso, não é assim. Se você ligar para malloc, receberá imediatamente a memória que geralmente era usada. Geralmente, você faz algumas coisas temporárias com a sua memória e imediatamente a traz de volta. E ela imediatamente retorna para a piscina de malloc, e o próximo ciclo de malloc a puxa novamente. Portanto, o uso real da memória é reduzido a um conjunto de objetos vivos em um determinado momento, além de vazamentos. E se tudo não fluir de maneira indecente, a maior parte da memória se instala em caches e no processador e funciona rapidamente. Mas requer muito gerenciamento de memória manual com malloc e free, chamado na ordem certa, no lugar certo. O próprio Rust pode lidar com isso corretamente e, em vários casos, oferece desempenho ainda maior, já que o consumo de memória é reduzido apenas aos cálculos atuais - em vez de esperar o próximo ciclo do GC liberar memória. Como resultado, temos uma maneira muito interessante de melhorar o desempenho. E bastante poderoso - no sentido em que fiz essas coisas ao processar dados para a fintech, e isso me permitiu acelerar cinco vezes. Essa é uma aceleração bastante grande, especialmente em um mundo onde os processadores não estão ficando mais rápidos, e todos continuamos aguardando melhorias.
Andrew : Eu também gostaria de perguntar sobre a carreira como um todo. Você ficou famoso por trabalhar no JIT na HotSpot e depois se mudar para a Azul - e essa também é uma empresa da JVM. Mas eles já estavam envolvidos em mais ferro do que software. E, de repente, mudou para Big Data e Machine Learning e, em seguida, para detecção de fraude. Como isso aconteceu? Essas são áreas muito diferentes de desenvolvimento.
Cliff : Estou programando há algum tempo e consegui fazer check-in em classes muito diferentes. E quando as pessoas dizem: "Ah, você é quem criou o JIT para Java!", É sempre engraçado. Mas antes disso, eu estava envolvido no clone PostScript - a linguagem que a Apple já usou para suas impressoras a laser. E antes disso ele fez a implementação da linguagem Forth. Eu acho que o tema comum para mim é o desenvolvimento de ferramentas. Durante toda a minha vida, desenvolvi ferramentas com as quais outras pessoas escrevem seus programas legais. Mas eu também estava envolvido no desenvolvimento de sistemas operacionais, drivers, depuradores no nível do kernel, linguagens para o desenvolvimento do sistema operacional, que começaram trivialmente, mas com o tempo tudo ficou complicado e complicado. Mas o tópico principal, no entanto, é o desenvolvimento de ferramentas. Uma grande parte da vida passou entre Azul e Sun, e era sobre Java. Mas quando iniciei o Big Data e o Machine Learning, coloquei meu chapéu de novo e disse: “Ah, e agora temos um problema não trivial, e aqui muitas coisas interessantes e pessoas que fazem alguma coisa” acontecem. Este é um ótimo caminho de desenvolvimento que vale a pena seguir.
Sim, eu realmente gosto de computação distribuída. Meu primeiro trabalho foi como estudante em C, em um projeto de publicidade. Esses computadores foram distribuídos em chips Zilog Z80, que coletavam dados para o reconhecimento óptico de texto analógico produzido por um analisador analógico real. Foi um tópico legal e totalmente anormal. Mas havia problemas, uma parte não era reconhecida corretamente, por isso era necessário tirar uma foto e mostrá-la a uma pessoa que já lia com os olhos e informava o que foi dito lá; portanto, havia malabaristas de dados e esse trabalho tinha seu próprio idioma. . Havia um back-end que lidava com tudo isso - funcionando paralelamente ao Z80 com terminais vt100 em execução - um por pessoa, e havia um modelo de programação paralelo no Z80. Um determinado pedaço de memória comum compartilhado por todos os Z80 dentro de uma configuração em estrela; o plano de fundo foi compartilhado e metade da RAM foi compartilhada na rede, e outra metade foi privada ou gasta em outra coisa. Um sistema distribuído paralelo significativamente complexo com ... memória semi-compartilhada compartilhada. Quando foi ... Já não me lembro, em algum lugar em meados dos anos 80. Faz muito tempo.
Sim, assumiremos que 30 anos é um longo tempo.As tarefas associadas à computação distribuída existem há muito tempo, as pessoas lutam há muito tempo com os clusters Beowulf . Esses clusters se parecem com ... Por exemplo: existe Ethernet e seu x86 rápido está conectado a essa Ethernet, e agora você deseja obter memória compartilhada falsa, porque ninguém pode lidar com a codificação da computação distribuída, era muito complicado e, portanto, era memória compartilhada falsa com proteção páginas de memória x86 e, se você escreveu nesta página, dissemos aos outros processadores que, se tivessem acesso à mesma memória compartilhada, seria necessário fazer o download de você e, assim, algo como um protocolo de suporte à coerência de cache seria exibido. e software para isso. Conceito interessante. O problema real, é claro, era diferente. Tudo isso funcionou, mas você rapidamente teve problemas de desempenho, porque ninguém entendeu os modelos de desempenho em um nível suficientemente bom - quais padrões de acesso à memória existem, como garantir que os nós não façam ping um ao outro sem parar, e assim por diante.
No H2O, eu vim com isso: os próprios desenvolvedores são responsáveis por determinar onde o paralelismo está oculto e onde não está. Eu vim com um modelo de codificação que escrever código de alto desempenho era fácil e simples. Mas escrever código lento é difícil, vai parecer ruim. Você precisa tentar seriamente escrever código lento, precisa usar métodos não padrão. O código de frenagem é visível de relance. Como resultado, geralmente é escrito um código que funciona rapidamente, mas você precisa descobrir o que fazer no caso de memória compartilhada. Tudo isso está vinculado a matrizes grandes e o comportamento é semelhante a matrizes grandes não voláteis em Java paralelo. Quero dizer, imagine que dois threads gravem em um array paralelo, um deles ganha e o outro, respectivamente, perde, e você não sabe qual deles é quem. Se não forem voláteis, o pedido pode ser qualquer coisa - e realmente funciona bem. As pessoas realmente se preocupam com a ordem das operações, definem a volatilidade corretamente e esperam problemas de memória nos lugares certos. Caso contrário, eles simplesmente escreveriam o código na forma de ciclos de 1 a N, onde N é alguns trilhões, na esperança de que todos os casos complexos se tornem paralelos automaticamente - e isso não funciona lá. Mas no H2O isso não é Java nem Scala, você pode considerá-lo “Java menos menos”, se desejar. Esse é um estilo de programação muito compreensível e é semelhante à escrita de código C ou Java simples com loops e matrizes. Mas, ao mesmo tempo, a memória pode ser processada com terabytes. Eu ainda uso H2O. – , . Big Data , H2O.
: ?
: ? , – .
. . , , , , . Sun, , , , . , , . , C1, , – . , . , x86- , , 5-10 , 50 .
, , , , C. , , - , C . C, C . , , C, - … , . , . , , . , , 5% . - – , « », , . : , , . . , – , . , . - – . , , ( , ), , , . , , , .
, , , , , , . , , , - . , , , . , , , , . , : , . , , - : , , - , . – , , – ! – , . Java. Java , , , , – , « ». , , . , Java C . – Java, C , , , . , – , . , . , , . : .
: - . , , - , ?
: ! – , NP- - . , ? . , Ahead of Time – . - . , , – , ! – , . , , . . ? , : , , - ! - , . . , , . : - , - . , , . , , , , - . ! , , , – . . NP- .
: , – . , , , , …
: . «». . , . – , , , ( , ). , - . , , , . , , . , . , , . , , . , , - , – . – . , GC, , , , – , . , . , , . , – , ? , .
: , ? ?
: GPU , !
: . ?
: , - Azul. , . . H2O , . , GPU. ? , Azul, : – .
: ?
: , … . , . , , , , . , , . , Java C1 C2 – . , Java – . , , – . … . - , Sun, … , , . , . , . … … , . , , . . - , : . , , , , , , . , . . , . « , , ». : «!». , , , : , .
– , , , . . , , , , . , Java JIT, C2. , – . , – ! . , , , , , , . . . , . , , , , : , , . , – . , , - . : « ?». , . , , : , , – ? , . , , , , , , - .
: , -. ?
: , , . – . . , . . . : , , - – . . , , – , . , , , , - , . , . , , - . , , – , .
, . , – , , . , . , – . , . , , « », , – , , , , . , , « ».
. . - , , «»: , – . – . , , . «, -, , ». , : , . , , . . – , . , ? , ? ? , ? . , . – . . , . – – , . , « » . : «--», : «, !» . . , , , , . , . , . , – , . – , . , , , .
, – , . , , . , . , , , , . , , . , , , , . . , , , . , , , , . , , , . , – , , , . , .
: … . , . . Hydra!
Hydra 2019, 11-12 2019 -. «The Azul Hardware Transactional Memory experience» . .