Ótima entrevista com Cliff Click, o pai da compilação JIT em Java

Ótima entrevista com Cliff Click, o pai da compilação JIT em JavaClique no penhasco — CTO da Cratus (sensores IoT para melhoria de processos), fundador e cofundador de diversas startups (incluindo Rocket Realtime School, Neurensic e H2O.ai) com diversas saídas de sucesso. Cliff escreveu seu primeiro compilador aos 15 anos (Pascal para o TRS Z-80)! Ele é mais conhecido por seu trabalho em C2 em Java (o Sea of ​​​​Nodes IR). Este compilador mostrou ao mundo que o JIT poderia produzir código de alta qualidade, o que foi um dos fatores para o surgimento do Java como uma das principais plataformas de software modernas. Então Cliff 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 em 10 milissegundos. Em geral, Cliff conseguiu trabalhar em todos os aspectos da JVM.

 
Este habrapost é uma ótima entrevista com Cliff. Falaremos sobre os seguintes temas:

  • Transição para otimizações de baixo nível
  • Como fazer uma grande refatoração
  • Modelo de custo
  • Treinamento de otimização de baixo nível
  • Exemplos práticos de melhoria de desempenho
  • 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 multi-cores
  • O maior desafio da vida

A entrevista é conduzida por:

  • Andrey Satarin da Amazon Web Services. Em sua carreira, conseguiu atuar em projetos completamente diferentes: testou o banco de dados distribuído NewSQL no Yandex, um sistema de detecção de nuvem na Kaspersky Lab, um jogo multiplayer no Mail.ru e um serviço de cálculo de preços de câmbio no Deutsche Bank. Interessado em testar back-end e sistemas distribuídos em larga escala.
  • Vladimir Sitnikov do Netcracker. Dez anos de trabalho no desempenho e escalabilidade do NetCracker OS, software utilizado pelas operadoras de telecomunicações para automatizar processos de gerenciamento de redes e equipamentos de rede. Interessado em problemas de desempenho de Java e Oracle Database. Autor de mais de uma dúzia de melhorias de desempenho no driver PostgreSQL JDBC oficial.

Transição para otimizações de baixo nível

Andrew: Você é um grande nome no mundo da compilação JIT, Java e do trabalho de desempenho em geral, certo? 

Penhasco: É assim!

Andrew: Vamos começar com algumas questões gerais sobre trabalho performático. O que você acha da escolha entre otimizações de alto e baixo nível, como trabalhar no nível da CPU?

Penhasco: Sim, tudo é simples aqui. O código mais rápido é aquele que nunca é executado. Portanto, você sempre precisa começar de um nível alto, trabalhar em algoritmos. Uma notação O melhor superará uma notação O pior, a menos que intervenham algumas constantes suficientemente grandes. As coisas de baixo nível ficam por último. Normalmente, se você otimizou o resto da sua pilha bem o suficiente e ainda resta algumas coisas interessantes, esse é um nível baixo. Mas como começar de alto nível? Como você sabe que foi feito trabalho de alto nível suficiente? Bem... de jeito nenhum. Não existem receitas prontas. Você precisa entender o problema, decidir o que vai fazer (para não tomar medidas desnecessárias no futuro) e então poderá descobrir o criador de perfil, que pode dizer algo útil. Em algum momento, você mesmo percebe que se livrou de coisas desnecessárias e é hora de fazer alguns ajustes de baixo nível. Este é definitivamente um tipo especial de arte. Tem muita gente fazendo coisas desnecessárias, mas indo tão rápido que não tem tempo para se preocupar com produtividade. Mas isso é até que a questão surja sem rodeios. Normalmente, 99% das vezes ninguém se importa com o que eu faço, até o momento em que surge algo importante no caminho crítico com o qual ninguém se importa. E aqui todo mundo começa a incomodar você sobre “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 pistas! Você está apenas tentando fazer algo funcionar e, no processo, descobre o que é importante. Nunca dá para saber de antemão que essa peça precisa ser perfeita, então, na verdade, é preciso ser perfeito em tudo. Mas isso é impossível e você não faz isso. Sempre há muitas coisas para consertar – e isso é completamente normal.

Como fazer uma grande refatoração

Andrew: Como você trabalha em uma performance? Este é um problema transversal. Por exemplo, você já teve que trabalhar em problemas que surgem da intersecção de muitas funcionalidades existentes?

Penhasco: Eu tento evitá-lo. Se eu sei que o desempenho será um problema, penso nisso antes de começar a codificar, especialmente com estruturas de dados. Mas muitas vezes você descobre tudo isso muito mais tarde. E então você tem que tomar medidas extremas e fazer o que chamo de “reescrever e conquistar”: você precisa pegar um pedaço grande o suficiente. Parte do código ainda terá que ser reescrito devido a problemas de desempenho ou qualquer outra coisa. Seja qual for o motivo para reescrever o código, quase sempre é melhor reescrever uma parte maior do que uma parte menor. Nesse momento, todos começam a tremer de medo: “ai meu Deus, não dá para mexer em tanto código!” Mas, na verdade, esta abordagem quase sempre funciona muito melhor. Você precisa assumir imediatamente um grande problema, desenhar um grande círculo ao redor dele e dizer: vou reescrever tudo dentro do círculo. A borda é muito menor do que o conteúdo dentro dela que precisa ser substituído. E se essa delimitação de limites permite que você faça o trabalho interno perfeitamente, suas mãos estão livres, faça o que quiser. Depois de entender o problema, o processo de reescrita fica muito mais fácil, então dê uma boa mordida!
Ao mesmo tempo, quando você faz uma grande reescrita e percebe que o desempenho será um problema, você pode imediatamente começar a se preocupar com isso. Isso geralmente se transforma em coisas simples como “não copie dados, gerencie os dados da maneira mais simples possível, torne-os pequenos”. Em grandes reescritas, existem maneiras padrão de melhorar o desempenho. E quase sempre giram em torno de dados.

Modelo de custo

Andrew: Em um dos podcasts você falou sobre modelos de custos no contexto da produtividade. Você pode explicar o que você quis dizer com isso?

Penhasco: Certamente. Nasci em uma época em que o desempenho do processador era extremamente importante. E esta era retorna novamente - o destino tem ironia. Comecei a viver na época das máquinas de oito bits; meu primeiro computador trabalhava com 256 bytes. Exatamente bytes. Tudo era muito pequeno. As instruções tiveram que ser contadas e, à medida que começamos a subir na pilha de linguagens de programação, as linguagens foram ganhando cada vez mais. Houve o Assembler, depois o Basic, depois o C, e o C cuidou de muitos detalhes, como alocação de registradores e seleção de instruções. Mas tudo estava bem claro ali, e se eu apontasse para uma instância de uma variável, receberia carga e o custo dessa instrução seria conhecido. O hardware produz um certo número de ciclos de máquina, portanto a velocidade de execução de diferentes coisas pode ser calculada simplesmente somando todas as instruções que você irá executar. Cada comparação/teste/ramificação/chamada/carregamento/armazenamento pode ser somada e dizer: esse é o tempo de execução para você. Ao trabalhar para melhorar o desempenho, você definitivamente prestará atenção em quais números correspondem a pequenos ciclos quentes. 
Mas assim que você muda para Java, Python e coisas semelhantes, você rapidamente se afasta do hardware de baixo nível. Qual é o custo de chamar um getter em Java? Se o JIT no HotSpot estiver correto embutido, ele será carregado, mas se não fizer isso, será uma chamada de função. Como a chamada está em um loop ativo, ela substituirá todas as outras otimizações nesse loop. Portanto, o custo real será muito maior. E você perde imediatamente a capacidade de olhar para um trecho de código e entender que devemos executá-lo em termos de velocidade de clock do processador, memória e cache usado. Tudo isso só se torna interessante se você realmente entrar na performance.
Agora nos encontramos em uma situação em que as velocidades dos processadores quase não aumentaram durante 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 de repente você entrar na computação paralela, é incrivelmente difícil, todo mundo olha para você como se fosse James Bond. Acelerações dez vezes maiores aqui geralmente ocorrem em lugares onde alguém estragou alguma coisa. A simultaneidade requer muito trabalho. Para obter essa aceleração de XNUMXx, você precisa entender o modelo de custo. Quanto e quanto custa? E para fazer isso, você precisa entender como a língua se ajusta ao hardware subjacente.
Martin Thompson escolheu uma ótima palavra para seu blog Simpatia Mecânica! Você precisa entender o que o hardware fará, como exatamente ele fará isso e por que ele faz o que faz. Usando isso, é bastante fácil começar a contar instruções e descobrir para onde está indo o tempo de execução. Se você não tiver o treinamento adequado, estará apenas procurando um gato preto em um quarto escuro. Vejo pessoas otimizando o desempenho o tempo todo e que não têm ideia do que estão fazendo. Eles sofrem muito e não estão progredindo muito. E quando eu pego o mesmo trecho de código, coloco alguns pequenos hacks e obtenho uma aceleração de cinco ou dez vezes, eles dizem: bem, isso não é justo, já sabíamos que você era melhor. Incrível. Do que estou falando... o modelo de custo é sobre que tipo de código você escreve e quão rápido ele é executado, em média, no quadro geral.

Andrew: E como você consegue manter tanto volume na cabeça? Isso é conseguido com mais experiência ou? De onde vem essa experiência?

Penhasco: Bem, não obtive minha experiência da maneira mais fácil. Eu programei em Assembly na época em que você conseguia entender cada instrução. Parece estúpido, mas desde então o conjunto de instruções do Z80 sempre ficou na minha cabeça, na minha memória. Não me lembro dos nomes das pessoas depois de um minuto de conversa, mas lembro-me do código escrito há 40 anos. É engraçado, parece uma síndrome"cientista idiota".

Treinamento de otimização de baixo nível

Andrew: Existe uma maneira mais fácil de entrar?

Penhasco: Sim e não. O hardware que todos usamos não mudou muito ao longo do tempo. Todo mundo usa x86, com exceção dos smartphones Arm. Se você não está fazendo algum tipo de incorporação hardcore, você está fazendo a mesma coisa. Ok, próximo. As instruções também não mudaram durante séculos. Você precisa escrever algo no Assembly. Não muito, mas o suficiente para começar a entender. Você está sorrindo, mas estou falando completamente a sério. Você precisa entender a correspondência entre linguagem e hardware. Depois disso você precisa escrever um pouco e fazer um pequeno compilador de brinquedo para uma pequena linguagem de brinquedo. Semelhante a um brinquedo significa que precisa ser feito em um período de tempo razoável. Pode ser super simples, mas deve gerar instruções. O ato de gerar uma instrução ajudará você a entender o modelo de custo da ponte entre o código de alto nível que todos escrevem e o código de máquina executado no hardware. Essa correspondência será gravada no cérebro no momento em que o compilador for escrito. Até o compilador mais simples. Depois disso, você pode começar a olhar para Java e para o fato de que seu abismo semântico é muito mais profundo e é muito mais difícil construir pontes sobre ele. Em Java, é muito mais difícil entender se nossa ponte foi boa ou ruim, o que fará com que ela desmorone e o que não acontecerá. Mas você precisa de algum tipo de ponto de partida onde você olhe para o código e entenda: “sim, esse getter deve ser embutido sempre”. E então acontece que às vezes isso acontece, exceto na situação em que o método se torna muito grande e o JIT começa a incorporar tudo. O desempenho de tais locais pode ser previsto instantaneamente. Geralmente getters funcionam bem, mas então você olha para grandes hot loops e percebe que existem algumas chamadas de função flutuando por aí que não sabem o que estão fazendo. Este é o problema com o uso generalizado de getters, a razão pela qual eles não estão embutidos é que não está claro se eles são getters. Se você tiver uma base de código super pequena, basta lembrá-la e dizer: este é um getter e 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 tempo em algum loop e para entender o que esse loop está fazendo, precisamos observar cada função interna. É impossível compreender isso sem estudar a função, e isso retarda seriamente o processo de compreensão. É por isso que não uso getters e setters, alcancei um novo nível!
Onde obter o modelo de custo? Bem, você pode ler alguma coisa, claro... Mas acho que a melhor maneira é agir. Fazer um pequeno compilador será a melhor maneira de entender o modelo de custos e encaixá-lo em sua cabeça. Um pequeno compilador adequado para programar um micro-ondas é tarefa para iniciantes. Bem, quero dizer, se você já possui habilidades de programação, isso deve ser suficiente. Todas essas coisas, como analisar uma string que você tem como algum tipo de expressão algébrica, extrair instruções para operações matemáticas na ordem correta, extrair os valores corretos dos registradores - tudo isso é feito de uma vez. E enquanto você faz isso, isso ficará impresso em seu cérebro. Acho que todo mundo sabe o que um compilador faz. E isso dará uma compreensão do modelo de custo.

Exemplos práticos de melhoria de desempenho

Andrew: O que mais você deve prestar atenção ao trabalhar na produtividade?

Penhasco: Estruturas de dados. Aliás, sim, faz muito tempo que não dou essas aulas... Escola de Foguetes. Foi divertido, mas exigiu muito esforço, e eu também tenho uma vida! OK. Então, em uma das grandes e interessantes aulas, “Para onde vai seu desempenho”, dei um exemplo aos alunos: dois gigabytes e meio de dados de fintech foram lidos de um arquivo CSV e então eles tiveram que calcular o número de produtos vendidos . Dados regulares do mercado de ticks. Pacotes UDP convertidos para formato texto desde os anos 70. Bolsa Mercantil de Chicago – todos os tipos de coisas como manteiga, milho, soja, coisas assim. Foi necessário contabilizar esses produtos, o número de transações, o volume médio de movimentação de fundos e mercadorias, etc. É uma matemática de negociação bastante simples: encontre o código do produto (que tem de 1 a 2 caracteres na tabela hash), obtenha o valor, adicione-o a um dos conjuntos de negociação, adicione volume, adicione valor e algumas outras coisas. Matemática muito simples. A implementação do brinquedo foi muito simples: tudo está em um arquivo, eu leio o arquivo e o percorro, dividindo os registros individuais em strings Java, procurando neles as coisas necessárias e somando-as de acordo com a matemática descrita acima. E funciona em baixa velocidade.

Com essa abordagem, fica óbvio o que está acontecendo, e a computação paralela não vai ajudar, certo? Acontece que um aumento de cinco vezes no desempenho pode ser alcançado simplesmente escolhendo as estruturas de dados corretas. E isso surpreende até mesmo programadores experientes! No meu caso específico, o truque era não fazer alocações de memória em um hot loop. Bem, isso não é toda 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”, ou “uma vez por linha”, ou “uma vez por campo”, nada assim. É aqui que o tempo é gasto. Como isso funciona? Imagine eu fazendo uma ligação String.split() ou BufferedReader.readLine(). Readline cria uma string a partir de um conjunto de bytes que veio pela 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 estou jogando fora - bem, já processei, só isso. Então, para cada byte lido desses 2.7G, dois caracteres serão escritos na linha, ou seja, já 5.4G, e não preciso deles para mais nada, então são jogados fora. Se você observar a largura de banda da memória, carregamos 2.7 G que passa pela memória e pelo barramento de memória do processador, e então o dobro é enviado para a linha que está na memória, e tudo isso se desgasta quando cada nova linha é criada. Mas preciso ler, o hardware lê, mesmo que depois tudo se desgaste. E tenho que anotar porque criei uma linha e os caches estão cheios - o cache não comporta 2.7G. Então, para cada byte que leio, leio mais dois bytes e escrevo mais dois bytes, e no final eles ficam com uma proporção de 4:1 - nessa proporção estamos desperdiçando largura de banda de memória. E então acontece que se eu fizer isso String.split() – esta não é a última vez que faço isso, pode haver outros 6-7 campos dentro. Portanto, o código clássico de ler CSV e depois analisar as strings resulta em um desperdício de largura de banda de memória de cerca de 14:1 em relação ao que você realmente gostaria de ter. Se você jogar fora essas seleções, poderá obter uma aceleração cinco vezes maior.

E não é tão difícil. Se você olhar o código do ângulo certo, tudo se tornará bastante simples quando você perceber o problema. Você não deve parar de alocar memória completamente: o único problema é que você aloca algo e ele morre imediatamente, e ao longo do caminho queima um recurso importante, que neste caso é a largura de banda da memória. E tudo isso resulta em queda na produtividade. No x86 você geralmente precisa queimar ativamente os ciclos do processador, mas aqui você queimou toda a memória muito antes. A solução é reduzir a quantidade de descarga. 
A outra parte do problema é que se você executar o criador de perfil quando a faixa de memória acabar, exatamente quando isso acontecer, você geralmente estará esperando o cache voltar porque está cheio de lixo que você acabou de produzir, todas aquelas linhas. Portanto, cada operação de carregamento ou armazenamento torna-se lenta, pois leva a perdas de cache - todo o cache fica lento, esperando que o lixo saia dele. Portanto, o criador de perfil mostrará apenas ruído aleatório quente espalhado por todo o loop - não haverá instruções ou locais quentes separados no código. Apenas barulho. E se você olhar para os ciclos de GC, eles são todos da 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 ele 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. Nesse momento, algo deve se fechar na sua cabeça e soar: “por que isso?!” O estouro da faixa de memória não é exibido no depurador clássico; você precisa executar o depurador do contador de desempenho de hardware e ver você mesmo e diretamente. Mas isso não pode ser suspeitado diretamente por esses três sintomas. O terceiro sintoma é quando você olha o que destaca, pergunta ao criador de perfil e ele responde: “Você fez um bilhão de linhas, mas o GC funcionou de graça”. Assim que isso acontece, você percebe que criou muitos objetos e queimou toda a memória. Existe uma maneira de descobrir isso, mas não é óbvia. 

O problema está na estrutura de dados: a estrutura básica subjacente a tudo o que acontece é muito grande, tem 2.7 G no disco, então fazer uma cópia disso é muito indesejável - você deseja carregá-lo do buffer de bytes da rede imediatamente nos registros, para não ler e escrever na linha cinco vezes. Infelizmente, Java não oferece essa biblioteca como parte do JDK por padrão. Mas isso é trivial, certo? Essencialmente, essas são de 5 a 10 linhas de código que serão usadas para implementar seu próprio carregador de string em buffer, que repete o comportamento da classe de string, ao mesmo tempo em que é um wrapper em torno do buffer de bytes subjacente. Como resultado, você está trabalhando quase como se estivesse com strings, mas na verdade os ponteiros para o buffer estão se movendo para lá e os bytes brutos não são copiados em lugar nenhum e, portanto, os mesmos buffers são reutilizados continuamente e o sistema operacional fica feliz em assumir as tarefas para as quais foi projetado, como buffer duplo oculto desses buffers de bytes, e você não está mais lidando com um fluxo interminável de dados desnecessários. Aliás, você entende que ao trabalhar com GC é garantido que cada alocação de memória não ficará visível para o processador após o último ciclo de GC? Portanto, tudo isso não pode estar no cache e então ocorre uma falha 100% garantida. Ao trabalhar com um ponteiro, em x86, subtrair um registro da memória leva de 1 a 2 ciclos de clock e, assim que isso acontecer, você paga, paga, paga, porque a memória está toda ligada NOVE caches – e este é o custo da alocação de memória. Valor real.

Em outras palavras, as estruturas de dados são a coisa mais difícil de mudar. E quando você perceber que escolheu a estrutura de dados errada que prejudicará o desempenho mais tarde, geralmente há muito trabalho a ser feito, mas se não o fizer, as coisas vão piorar. Antes de mais nada é preciso pensar nas estruturas de dados, isso é importante. O principal custo aqui recai sobre estruturas de dados grossas, que estão começando a ser usadas no estilo “Copiei a estrutura de dados X para a estrutura de dados Y porque gosto mais do formato de Y”. Mas a operação de cópia (que parece barata) na verdade desperdiça largura de banda da memória e é aí que todo o tempo de execução desperdiçado é enterrado. Se eu tiver uma string gigante de JSON e quiser transformá-la em uma árvore DOM estruturada de POJOs ou algo assim, a operação de analisar essa string e construir o POJO, e então acessar o POJO novamente mais tarde, resultará em custos desnecessários - é não é barato. Exceto se você executar POJOs com muito mais frequência do que uma string. De imediato, você pode tentar descriptografar a string e extrair dela apenas o que precisa, sem transformá-la em nenhum POJO. Se tudo isso acontecer em um caminho que requer desempenho máximo, sem POJOs para você, você precisará de alguma forma se aprofundar na linha diretamente.

Por que criar sua própria linguagem de programação

Andrew: Você disse que para entender o modelo de custos, você precisa escrever sua própria linguagem...

Penhasco: Não é uma linguagem, mas um compilador. Uma linguagem e um compilador são duas coisas diferentes. A diferença mais importante está na sua cabeça. 

Andrew: A propósito, até onde eu sei, você está experimentando criar suas próprias linguagens. Para que?

Penhasco: Porque eu posso! Estou semi-aposentado, então esse é meu hobby. Tenho implementado linguagens de outras pessoas durante toda a minha vida. Também trabalhei muito no meu estilo de codificação. E também porque vejo problemas em outros idiomas. Vejo que existem maneiras melhores de fazer coisas familiares. E eu os usaria. Estou cansado de ver problemas em mim mesmo, em Java, em Python, em qualquer outra linguagem. Agora escrevo em React Native, JavaScript e Elm como um hobby que não tem a ver com aposentadoria, mas com trabalho ativo. Também escrevo em Python e, provavelmente, continuarei trabalhando em aprendizado de máquina para back-ends Java. Existem muitas linguagens populares e todas possuem recursos interessantes. Cada um é bom à sua maneira e você pode tentar reunir todos esses recursos. Então, estou estudando coisas que me interessam, o comportamento da linguagem, tentando chegar a uma semântica razoável. E até agora estou conseguindo! No momento estou lutando com a semântica da memória, porque quero tê-la como em C e Java e obter um modelo de memória forte e semântica de memória para cargas e armazenamentos. Ao mesmo tempo, tenha inferência automática de tipos como em Haskell. Aqui, estou tentando misturar inferência de tipo tipo Haskell com trabalho de memória em C e Java. Isto é o que tenho feito nos últimos 2-3 meses, por exemplo.

Andrew: Se você construir uma linguagem que tire melhores aspectos de outras linguagens, você acha que alguém fará o contrário: pegará suas ideias e as usará?

Penhasco: É exatamente assim que surgem novos idiomas! Por que Java é semelhante a C? Porque C tinha uma sintaxe boa que todos entendiam e Java foi inspirado nessa sintaxe, adicionando segurança de tipo, verificação de limites de array, GC, e eles também melhoraram algumas coisas de C. Eles adicionaram suas próprias. Mas eles se inspiraram bastante, certo? Todos estão sobre os ombros dos gigantes que vieram antes de vocês – é assim que o progresso é feito.

Andrew: Pelo que entendi, seu idioma estará seguro para a memória. Você já pensou em implementar algo como um verificador de empréstimo do Rust? Você já olhou para ele, o que você acha dele?

Penhasco: Bem, tenho escrito C há muito tempo, com todo esse malloc e free, e gerenciando manualmente o tempo de vida. Você sabe, 90-95% do tempo de vida controlado manualmente tem a mesma estrutura. E é muito, muito doloroso fazer isso manualmente. Gostaria que o compilador simplesmente lhe dissesse o que está acontecendo lá e o que você conseguiu com suas ações. Para algumas coisas, o verificador de empréstimo faz isso imediatamente. E deveria exibir informações automaticamente, entender tudo, e nem me sobrecarregar em apresentar esse entendimento. Ele deve fazer pelo menos uma análise de escape local e, somente se falhar, precisará adicionar anotações de tipo que descreverão o tempo de vida - e tal esquema é muito mais complexo do que um verificador de empréstimo ou mesmo qualquer verificador de memória existente. A escolha entre “está tudo bem” e “não entendo nada” - não, deve haver algo melhor. 
Então, como alguém que escreveu muito código em C, acho que ter suporte para controle automático de vida útil é o mais importante. Também estou farto da quantidade de memória que o Java usa e a principal reclamação é o GC. Ao alocar memória em Java, você não recuperará a memória que era local no último ciclo de GC. Este não é o caso de linguagens com gerenciamento de memória mais preciso. Se você chamar malloc, obterá imediatamente a memória que normalmente era usada. Normalmente você faz algumas coisas temporárias com a memória e imediatamente a devolve. E ele retorna imediatamente ao pool malloc, e o próximo ciclo malloc o extrai novamente. Portanto, o uso real da memória é reduzido ao conjunto de objetos vivos em um determinado momento, mais vazamentos. E se tudo não vazar de forma totalmente indecente, a maior parte da memória vai parar nos caches e no processador, e funciona rápido. Mas requer muito gerenciamento manual de memória com malloc e free chamados na ordem certa, no lugar certo. O Rust pode lidar com isso sozinho e, em muitos casos, fornecer um desempenho ainda melhor, já que o consumo de memória é reduzido apenas à computação atual - em vez de esperar pelo próximo ciclo de GC para liberar memória. Como resultado, obtivemos uma forma muito interessante de melhorar o desempenho. E bastante poderoso - quero dizer, eu fiz essas coisas ao processar dados para fintech, e isso me permitiu obter uma aceleração de cerca de cinco vezes. Isso é um grande impulso, especialmente em um mundo onde os processadores não estão ficando mais rápidos e ainda estamos aguardando melhorias.

Carreira de engenheiro de desempenho

Andrew: Eu também gostaria de perguntar sobre carreiras em geral. Você ganhou destaque com seu trabalho JIT na HotSpot e depois mudou para a Azul, que também é uma empresa JVM. Mas já estávamos trabalhando mais em hardware do que em software. E então, de repente, eles mudaram para Big Data e Machine Learning e, em seguida, para detecção de fraudes. Como isso aconteceu? Estas são áreas de desenvolvimento muito diferentes.

Penhasco: Eu programo há bastante tempo e consegui fazer muitas aulas diferentes. E quando as pessoas falam: “ah, foi você quem fez JIT para Java!”, é sempre engraçado. Mas antes disso, eu estava trabalhando em um clone do PostScript – a linguagem que a Apple já usou para suas impressoras a laser. E antes disso fiz uma implementação da linguagem Forth. Acho que o tema comum para mim é o desenvolvimento de ferramentas. Durante toda a minha vida venho criando ferramentas com as quais outras pessoas escrevem seus programas interessantes. Mas também estive envolvido no desenvolvimento de sistemas operacionais, drivers, depuradores em nível de kernel, linguagens para desenvolvimento de sistemas operacionais, que começaram triviais, mas com o tempo tornaram-se cada vez mais complexos. Mas o tema principal ainda é o desenvolvimento de ferramentas. Grande parte da minha vida passou entre a Azul e a Sun, e foi sobre Java. Mas quando entrei no Big Data e no Machine Learning, coloquei meu chapéu chique de volta e disse: “Ah, agora temos um problema não trivial e há muitas coisas interessantes acontecendo e pessoas fazendo coisas”. Este é um ótimo caminho de desenvolvimento a seguir.

Sim, eu realmente adoro computação distribuída. Meu primeiro trabalho foi como estudante de C, em um projeto publicitário. Tratava-se de computação distribuída em chips Zilog Z80 que coletavam dados para OCR analógico, produzidos por um analisador analógico real. Foi um assunto legal e completamente maluco. Mas havia problemas, alguma parte não era reconhecida corretamente, então tinha que tirar uma foto e mostrar para uma pessoa que já sabia ler com os olhos e relatar o que dizia, e portanto havia trabalhos com dados, e esses trabalhos tinham sua própria língua. Havia um backend que processava tudo isso - Z80s rodando em paralelo com terminais vt100 rodando - um por pessoa, e havia um modelo de programação paralela no Z80. Algum pedaço de memória comum compartilhado por todos os Z80s em uma configuração em estrela; O backplane também era compartilhado, e metade da RAM era compartilhada na rede e a outra metade era privada ou ia para outra coisa. Um sistema distribuído paralelo significativamente complexo com memória compartilhada... semi-compartilhada. Quando foi isso... nem me lembro, em algum lugar em meados dos anos 80. Há muito tempo. 
Sim, vamos supor que 30 anos seja muito tempo atrás. Os problemas relacionados à computação distribuída já existem há muito tempo; as pessoas estão em guerra há muito tempo com Beowulf-aglomerados. 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 conseguia fazer codificação de computação distribuída naquela época, era muito difícil e, portanto, havia era uma memória compartilhada falsa com páginas de memória de proteção em x86, e se você escrevesse nesta página, então dissemos a outros processadores que se eles acessassem a mesma memória compartilhada, ela precisaria ser carregada de você e, portanto, algo como um protocolo para suporte apareceu coerência de cache e software para isso. Conceito interessante. O verdadeiro problema, claro, era outro. Tudo isso funcionou, mas você rapidamente teve problemas de desempenho, porque ninguém entendia os modelos de desempenho em um nível bom o suficiente - quais padrões de acesso à memória existiam, como garantir que os nós não fizessem ping uns aos outros indefinidamente e assim por diante.

O que descobri no H2O é que são os próprios desenvolvedores os responsáveis ​​por determinar onde o paralelismo está oculto e onde não está. Eu criei um modelo de codificação que tornou a escrita de código de alto desempenho fácil e simples. Mas escrever código de execução lenta é difícil, pois ficará ruim. Você precisa tentar seriamente escrever código lento, terá que usar métodos não padronizados. O código de travagem é visível à primeira vista. Como resultado, você geralmente escreve código que roda rapidamente, mas precisa descobrir o que fazer no caso de memória compartilhada. Tudo isso está ligado a grandes arrays e o comportamento é semelhante ao de grandes arrays não voláteis em Java paralelo. Quero dizer, imagine que dois threads escrevem em um array paralelo, um deles vence e o outro, respectivamente, perde, e você não sabe qual é qual. Se eles não forem voláteis, a ordem pode ser a que você quiser - e isso funciona muito bem. As pessoas realmente se preocupam com a ordem das operações, colocam voláteis nos lugares certos e esperam problemas de desempenho relacionados à memória nos lugares certos. Caso contrário, eles simplesmente escreveriam código na forma de loops de 1 a N, onde N é alguns trilhões, na esperança de que todos os casos complexos se tornassem automaticamente paralelos - e isso não funciona aí. Mas em H2O isso não é Java nem Scala; você pode considerá-lo “Java menos menos” se quiser. Este é um estilo de programação muito claro e semelhante a escrever código C ou Java simples com loops e arrays. Mas, ao mesmo tempo, a memória pode ser processada em terabytes. Eu ainda uso H2O. Eu o uso de vez em quando em diferentes projetos - e ainda é o mais rápido, dezenas de vezes mais rápido que seus concorrentes. Se você estiver fazendo Big Data com dados colunares, é muito difícil superar o H2O.

Desafios Técnicos

Andrew: Qual foi o seu maior desafio em toda a sua carreira?

Penhasco: Estamos discutindo a parte técnica ou não técnica da questão? Eu diria que os maiores desafios não são técnicos. 
Quanto aos desafios técnicos. Eu simplesmente os derrotei. Eu nem sei qual foi o maior, mas houve alguns bem interessantes que levaram um pouco de tempo, luta mental. Quando fui para a Sun, tinha certeza de que faria um compilador rápido, e vários veteranos responderam que eu nunca teria sucesso. Mas segui esse caminho, escrevi um compilador no alocador de registro e foi bem rápido. Era tão rápido quanto o C1 moderno, mas o alocador era muito mais lento naquela época e, pensando bem, era um grande problema de estrutura de dados. Eu precisava dele para escrever um alocador gráfico de registros e não entendia o dilema entre expressividade e velocidade do código, que existia naquela época e era muito importante. Descobriu-se que a estrutura de dados geralmente excede o tamanho do cache em x86s daquela época e, portanto, se eu inicialmente presumisse que o alocador de registro funcionaria de 5 a 10 por cento do tempo total de jitter, então, na realidade, acabou sendo 50 por cento.

Com o passar do tempo, o compilador tornou-se mais limpo e eficiente, parou de gerar códigos terríveis em mais casos e o desempenho começou a se assemelhar cada vez mais ao produzido por um compilador C. A menos, é claro, que você escreva alguma porcaria que nem mesmo C acelera. . Se você escrever código como C, obterá desempenho como C em mais casos. E quanto mais você avançava, mais frequentemente obtinha código que coincidia assintoticamente com o nível C, o alocador de registradores começava a parecer algo completo... independentemente de seu código ser executado rápido ou lento. Continuei trabalhando no alocador para fazer seleções melhores. Ele se tornou cada vez mais lento, mas apresentou desempenho cada vez melhor em casos em que ninguém mais conseguia lidar com isso. Eu poderia mergulhar em um alocador de registros, enterrar um mês de trabalho lá e, de repente, todo o código começaria a ser executado 5% mais rápido. Isso acontecia repetidas vezes e o alocador de registros virou uma espécie de obra de arte - todos adoravam ou odiavam, e o pessoal da academia fazia perguntas sobre o tema “por que tudo é feito assim”, por que não varredura de linha, e qual é a diferença. A resposta ainda é a mesma: um alocador baseado na coloração do gráfico mais um trabalho muito cuidadoso com o código do buffer é igual a uma arma da vitória, a melhor combinação que ninguém pode derrotar. E isso é algo pouco óbvio. Todo o resto que o compilador faz são coisas bastante bem estudadas, embora também tenham sido levadas ao nível da arte. Sempre fiz coisas que deveriam transformar o compilador em uma obra de arte. Mas nada disso foi extraordinário – exceto o alocador de registro. O truque é ter cuidado rasgar sob carga e, se isso acontecer (posso explicar com mais detalhes se estiver interessado), isso significa que você pode alinhar de forma mais agressiva, sem o risco de cair em uma torção no cronograma de desempenho. Naquela época, havia um monte de compiladores em grande escala, cheios de bugigangas e apitos, que tinham alocadores de registro, mas ninguém mais conseguia fazer isso.

O problema é que se você adicionar métodos sujeitos a inlining, aumentando e aumentando a área inlining, o conjunto de valores usados ​​​​ultrapassa instantaneamente o número de registros e você terá que cortá-los. O nível crítico geralmente ocorre quando o alocador desiste, e um bom candidato para um derramamento vale outro, você venderá algumas coisas geralmente selvagens. O valor do inlining aqui é que você perde parte da sobrecarga, sobrecarga para chamar e salvar, você pode ver os valores internos e otimizá-los ainda mais. O custo do inlining é que um grande número de valores ativos é formado e, se o seu alocador de registro queimar mais do que o necessário, você perde imediatamente. Portanto, a maioria dos alocadores tem um problema: quando o inlining ultrapassa um determinado limite, tudo no mundo começa a ser reduzido e a produtividade pode ser jogada no vaso sanitário. Quem implementa o compilador adiciona algumas heurísticas: por exemplo, parar de inlinar, começando com algum tamanho suficientemente grande, pois as alocações vão estragar tudo. É assim que uma torção no gráfico de desempenho é formada - você inline, inline, o desempenho cresce lentamente - e então bum! – cai como um macaco rápido porque você alinhou demais. Era assim que tudo funcionava antes do advento do Java. Java requer muito mais inlining, então tive que tornar meu alocador muito mais agressivo para que ele se nivelasse em vez de travar, e se você inline demais, ele começa a derramar, mas então o momento de “não derramar mais” ainda chega. Esta é uma observação interessante e surgiu do nada, não é óbvia, mas valeu a pena. Adotei o inlining agressivo e isso me levou a lugares onde o desempenho de Java e C trabalham lado a lado. Eles estão muito próximos - posso escrever código Java que é significativamente mais rápido que código C e coisas assim, mas em média, no geral, eles são aproximadamente comparáveis. Acho que parte desse mérito é o alocador de registro, que me permite fazer o inline da maneira mais estúpida possível. Eu apenas incorporo tudo o que vejo. A questão aqui é se o alocador funciona bem, se o resultado é um código funcionando de forma inteligente. Esse foi um grande desafio: entender tudo isso e fazer funcionar.

Um pouco sobre alocação de registros e multi-cores

Vladimir: Problemas como alocação de registros parecem algum tipo de tópico eterno e interminável. Eu me pergunto se já houve uma ideia que parecia promissora e depois falhou na prática?

Penhasco: Certamente! A alocação de registradores é uma área na qual você tenta encontrar algumas heurísticas para resolver um problema NP-completo. E você nunca consegue uma solução perfeita, certo? Isto é simplesmente impossível. Olha, compilação Ahead of Time - também funciona mal. A conversa aqui é sobre alguns casos comuns. Sobre o desempenho típico, para que você possa medir algo que você acha que é um bom desempenho típico - afinal, você está trabalhando para melhorá-lo! A alocação de registros é um tópico sobre desempenho. Depois de ter o primeiro protótipo, ele funciona e pinta o que é necessário, começa o trabalho de performance. Você precisa aprender a medir bem. Por que isso é importante? Se você tiver dados claros, você pode olhar para diferentes áreas e ver: sim, ajudou aqui, mas foi aí que tudo quebrou! Surgem algumas boas ideias, você adiciona novas heurísticas e de repente tudo começa a funcionar um pouco melhor na média. Ou não começa. Tive vários casos em que estávamos lutando pelo desempenho de XNUMX% que diferenciava nosso desenvolvimento do alocador anterior. E toda vez fica assim: em algum lugar você ganha, em algum lugar você perde. Se você tiver boas ferramentas de análise de desempenho, poderá encontrar as ideias perdedoras e entender por que elas falham. Talvez valha a pena deixar tudo como está, ou talvez adotar uma abordagem mais séria para fazer ajustes finos, ou sair e consertar alguma outra coisa. É um monte de coisas! Eu fiz esse hack legal, mas também preciso deste, e deste, e deste - e a combinação total deles dá algumas melhorias. E os solitários podem falhar. Esta é a natureza do trabalho de desempenho em problemas NP-completos.

Vladimir: Tem-se a sensação de que coisas como pintar em alocadores são um problema que já foi resolvido. Bem, está decidido por você, a julgar pelo que você está dizendo, então vale a pena...

Penhasco: Não está resolvido como tal. É você quem deve transformar isso em “resolvido”. Existem problemas difíceis e precisam ser resolvidos. Feito isso, é hora de trabalhar a produtividade. Você precisa abordar esse trabalho de acordo - fazer benchmarks, coletar métricas, explicar situações em que, ao reverter para uma versão anterior, seu hack antigo começou a funcionar novamente (ou vice-versa, parou). E não desista até conseguir algo. Como já disse, se tem ideias legais que não deram certo, mas no campo de alocação de registros de ideias é aproximadamente infinito. Você pode, por exemplo, ler publicações científicas. Embora agora esta área tenha começado a se mover muito mais lentamente e tenha se tornado mais clara do que na sua juventude. No entanto, existem inúmeras pessoas trabalhando nesta área e vale a pena tentar todas as suas ideias, todas estão esperando nos bastidores. E você não pode dizer o quão bons eles são, a menos que experimente. Eles se integram bem com todo o resto no seu alocador, porque um alocador faz muitas coisas, e algumas ideias no seu alocador específico não funcionarão, mas em outro alocador funcionarão facilmente. A principal maneira de vencer para o alocador é puxar o material lento para fora do caminho principal e forçá-lo a se dividir ao longo dos limites dos caminhos lentos. Então, se você quiser executar um GC, seguir o caminho lento, desotimizar, lançar uma exceção, todas essas coisas - você sabe que essas coisas são relativamente raras. E eles são muito raros, eu verifiquei. Você faz um trabalho extra e isso remove muitas das restrições nesses caminhos lentos, mas isso realmente não importa porque eles são lentos e raramente percorridos. Por exemplo, um ponteiro nulo – isso nunca acontece, certo? Você precisa ter vários caminhos para coisas diferentes, mas eles não devem interferir no principal. 

Vladimir: O que você acha dos multinúcleos, quando existem milhares de núcleos ao mesmo tempo? Isso é algo útil?

Penhasco: O sucesso da GPU mostra que ela é bastante útil!

Vladimir: Eles são bastante especializados. E quanto aos processadores de uso geral?

Penhasco: Bom, esse era o modelo de negócio da Azul. A resposta surgiu numa época em que as pessoas adoravam um desempenho previsível. Era difícil escrever código paralelo naquela época. O modelo de codificação H2O é altamente escalável, mas não é um modelo de uso geral. Talvez um pouco mais geral do que quando se usa uma GPU. Estamos falando da complexidade de desenvolver tal coisa ou da complexidade de usá-lo? Por exemplo, Azul me ensinou uma lição interessante, mas pouco óbvia: caches pequenos são normais. 

O maior desafio da vida

Vladimir: E quanto aos desafios não técnicos?

Penhasco: O maior desafio não era ser... gentil e legal com as pessoas. E como resultado, constantemente me encontrei em situações extremamente conflituosas. Aqueles em que eu sabia que as coisas estavam dando errado, mas não sabia como resolver esses problemas e não conseguia lidar com eles. Muitos problemas de longo prazo, que duram décadas, surgiram desta forma. O fato de Java possuir compiladores C1 e C2 é uma consequência direta disso. O fato de não ter havido compilação multinível em Java por dez anos consecutivos também é uma consequência direta. É óbvio que precisávamos de tal sistema, mas não é óbvio por que razão não existiu. Tive problemas com um engenheiro... ou com um grupo de engenheiros. Era uma vez, quando comecei a trabalhar na Sun, eu era... Ok, não só naquela época, geralmente sempre tenho minha opinião sobre tudo. E eu pensei que era verdade que você poderia simplesmente pegar essa sua verdade e contá-la de frente. Especialmente porque eu estava surpreendentemente certo na maior parte do tempo. E se você não gosta dessa abordagem... especialmente se você está obviamente errado e fazendo besteiras... Em geral, poucas pessoas tolerariam essa forma de comunicação. Embora alguns pudessem, como eu. Construí toda a minha vida sobre princípios meritocráticos. Se você me mostrar algo errado, eu imediatamente me virarei e direi: você disse bobagem. Ao mesmo tempo, é claro, peço desculpas e tudo mais, anotarei os méritos, se houver, e tomarei outras ações corretas. Por outro lado, estou surpreendentemente certo sobre uma porcentagem chocantemente grande do tempo total. E não funciona muito bem no relacionamento com as pessoas. Não estou tentando ser legal, mas estou fazendo a pergunta sem rodeios. “Isso nunca vai funcionar, porque um, dois e três.” E eles disseram, “Oh!” Houve outras consequências que provavelmente seria melhor ignorar: por exemplo, aquelas que levaram ao divórcio da minha esposa e aos dez anos de depressão depois disso.

O desafio é uma luta com as pessoas, com a percepção delas sobre o que você pode ou não fazer, o que é importante e o que não é. Houve muitos desafios em relação ao estilo de codificação. Eu ainda escrevo muito código, e naquela época até tive que desacelerar porque estava fazendo muitas tarefas paralelas e mal, em vez de focar em uma. Olhando para trás, escrevi metade do código do comando Java JIT, o comando C2. O próximo codificador mais rápido escreveu com metade da lentidão, o próximo com a metade da lentidão e foi um declínio exponencial. A sétima pessoa nesta fila foi muito, muito lenta – isso sempre acontece! Eu toquei muito código. Observei quem escreveu o quê, sem exceção, observei o código deles, revisei cada um deles e ainda continuei a escrever mais do que qualquer um deles. Essa abordagem não funciona muito bem com as pessoas. Algumas pessoas não gostam disso. E quando eles não conseguem lidar com isso, começam todos os tipos de reclamações. Por exemplo, uma vez me disseram para parar de programar porque eu estava escrevendo muito código e isso estava prejudicando a equipe, e tudo isso me pareceu uma piada: cara, se o resto da equipe desaparecer e eu continuar escrevendo código, você só perderei metade das equipes. Por outro lado, se eu continuar escrevendo código e você perder metade da equipe, isso soará como um péssimo gerenciamento. Eu realmente nunca pensei sobre isso, nunca falei sobre isso, mas ainda estava em algum lugar na minha cabeça. O pensamento estava girando no fundo da minha mente: “Vocês estão brincando comigo?” Então, o maior problema era eu e meu relacionamento com as pessoas. Agora eu me entendo muito melhor, fui líder de equipe de programadores por muito tempo e agora digo diretamente às pessoas: você sabe, eu sou quem sou e você terá que lidar comigo - está tudo bem se eu aguentar aqui? E quando começaram a lidar com isso, tudo funcionou. Na verdade, não sou mau nem bom, não tenho más intenções ou aspirações egoístas, é apenas a minha essência e preciso conviver com isso de alguma forma.

Andrew: Recentemente, todo mundo começou a falar sobre autoconsciência para introvertidos e habilidades interpessoais em geral. O que você pode dizer sobre isso?

Penhasco: Sim, esse foi o insight e a lição que aprendi com meu divórcio de minha esposa. O que aprendi com o divórcio foi me compreender. Foi assim que comecei a entender as outras pessoas. Entenda como funciona essa interação. Isso levou a descobertas uma após a outra. Houve uma consciência de quem sou e do que represento. O que estou fazendo: ou estou preocupado com a tarefa, ou evito conflitos, ou qualquer outra coisa - e esse nível de autoconsciência realmente ajuda a me manter no controle. Depois disso tudo fica muito mais fácil. Uma coisa que descobri não só em mim, mas também em outros programadores, é a incapacidade de verbalizar pensamentos quando você está sob estresse emocional. Por exemplo, você está sentado lá codificando, em um estado de fluxo, e então eles vêm correndo até você e começam a gritar histéricos que algo está quebrado e agora medidas extremas serão tomadas contra você. E você não pode dizer uma palavra porque está em estado de estresse emocional. Os conhecimentos adquiridos permitem-lhe preparar-se para este momento, sobreviver-lhe e partir para um plano de retiro, após o qual poderá fazer alguma coisa. Então, sim, quando você começa a perceber como tudo funciona, é um grande evento de mudança de vida. 
Eu mesmo não consegui encontrar as palavras certas, mas lembrei-me da sequência de ações. A questão é que essa reação é tanto física quanto verbal, e você precisa de espaço. Tal espaço, no sentido Zen. Isso é exatamente o que precisa ser explicado e, em seguida, afaste-se imediatamente - afaste-se puramente fisicamente. Quando permaneço em silêncio verbalmente, consigo processar a situação emocionalmente. À medida que a adrenalina chega ao seu cérebro, coloca você no modo de luta ou fuga, você não pode mais dizer nada, não - agora você é um idiota, um engenheiro de chicotadas, incapaz de uma resposta decente ou mesmo de parar o ataque, e o atacante está livre para atacar de novo e de novo. Você deve primeiro voltar a ser você mesmo, recuperar o controle, sair do modo “lutar ou fugir”.

E para isso precisamos de espaço verbal. Apenas espaço livre. Se você disser alguma coisa, então você pode dizer exatamente isso, e então ir e realmente encontrar “espaço” para você: dar um passeio no parque, trancar-se no chuveiro – não importa. O principal é desconectar-se temporariamente dessa situação. Assim que você desliga por pelo menos alguns segundos, o controle retorna, você começa a pensar sobriamente. “Ok, não sou nenhum tipo de idiota, não faço coisas estúpidas, sou uma pessoa muito útil.” Depois de conseguir se convencer, é hora de passar para a próxima etapa: entender o que aconteceu. Você foi atacado, o ataque veio de onde você não esperava, foi uma emboscada desonesta e vil. Isto é mau. A próxima etapa é entender por que o invasor precisava disso. Sério, por quê? Talvez porque ele próprio esteja furioso? Por que ele está bravo? Por exemplo, porque ele se estragou e não pode aceitar a responsabilidade? Esta é a maneira de lidar com toda a situação com cuidado. Mas isso requer margem de manobra, espaço verbal. O primeiro passo é interromper o contato verbal. Evite discussão com palavras. Cancele e vá embora o mais rápido possível. Se for uma conversa telefônica, basta desligar - essa é uma habilidade que aprendi ao me comunicar com minha ex-mulher. Se a conversa não estiver indo bem, basta dizer “tchau” e desligar. Do outro lado do telefone: “blá, blá, blá”, você responde: “sim, tchau!” e desligue. Você acabou de encerrar a conversa. Cinco minutos depois, quando a capacidade de pensar com sensatez volta para você, você já esfriou um pouco, é possível pensar em tudo, no que aconteceu e no que vai acontecer a seguir. E comece a formular uma resposta ponderada, em vez de apenas reagir por emoção. Para mim, o avanço na autoconsciência foi justamente o fato de que em caso de estresse emocional não consigo falar. Sair desse estado, pensar e planejar como responder e compensar os problemas são os passos certos no caso em que você não consegue falar. A maneira mais fácil é fugir da situação em que o estresse emocional se manifesta e simplesmente parar de participar desse estresse. Depois disso você se torna capaz de pensar, quando você consegue pensar, você se torna capaz de falar, e assim por diante.

A propósito, no tribunal, o advogado adversário tenta fazer isso com você - agora está claro o porquê. Porque ele tem a capacidade de suprimir você a tal estado que você nem consegue pronunciar seu nome, por exemplo. Num sentido muito real, você não conseguirá falar. Se isso acontecer com você, e se você sabe que se encontrará em um lugar onde as batalhas verbais estão acontecendo, em um lugar como um tribunal, então você pode vir com seu advogado. O advogado irá defendê-lo e impedir o ataque verbal, e fará isso de forma totalmente legal, e o espaço Zen perdido retornará para você. Por exemplo, tive que ligar para minha família algumas vezes, o juiz foi bastante amigável sobre isso, mas o advogado adversário gritou e gritou comigo, eu não consegui nem dizer uma palavra. Nestes casos, usar um mediador funciona melhor para mim. O mediador interrompe toda essa pressão que cai sobre você em um fluxo contínuo, você encontra o espaço Zen necessário e com ele a capacidade de falar retorna. É todo um campo do conhecimento em que há muito o que estudar, muito o que descobrir dentro de si, e tudo isso se transforma em decisões estratégicas de alto nível e diferentes para cada pessoa. Algumas pessoas não têm os problemas descritos acima; geralmente, as pessoas que são vendedores profissionais não os têm. Todas essas pessoas que vivem da palavra – cantores famosos, poetas, líderes religiosos e políticos, sempre têm algo a dizer. Eles não têm esses problemas, mas eu tenho.

Andrew: Foi... inesperado. Ótimo, já conversamos muito e é hora de encerrar esta entrevista. Certamente nos encontraremos na conferência e poderemos continuar esse diálogo. Vejo você na Hidra!

Você pode continuar sua conversa com Cliff na conferência Hydra 2019, que será realizada de 11 a 12 de julho de 2019 em São Petersburgo. Ele virá com um relatório "A experiência da memória transacional de hardware Azul". Os ingressos podem ser adquiridos no site oficial.

Fonte: habr.com

Adicionar um comentário