Cliff Click — CTO da Cratus (sensores IoT para aprimorar processos), fundador e cofundador 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 pelo trabalho no C2 em Java (o Sea of Nodes IR). Este compilador mostrou ao mundo que o JIT podia gerar código de qualidade, o que se tornou um dos fatores para o Java se tornar uma das principais plataformas de software modernas. Depois, Cliff ajudou a Azul Systems a construir um mainframe de 864 núcleos com software em Java puro, que suportava pausas de GC em um heap de 500 gigabytes dentro de 10 milissegundos. No geral, Cliff conseguiu trabalhar em todos os aspectos da JVM.
Esta postagem no Habr é uma grande entrevista com Cliff. Vamos discutir os seguintes tópicos:
- Transição para otimizações de baixo nível
- Como fazer um grande refatoramento
- Modelo de custo
- Aprendizado sobre otimizações 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 registradores e multiprocessamento
- O maior desafio da vida
Entrevista conduzida por:
- Andrei Satarin da Amazon Web Services. Em sua carreira, trabalhou em projetos bastante diversos: testou um banco de dados distribuído NewSQL no Yandex, um sistema de detecção em nuvem no Kaspersky Lab, um jogo multiplayer na Mail.ru e um serviço de cálculo de preços em moedas no Deutsche Bank. Tem interesse em testar sistemas backend de larga escala e sistemas distribuídos.
- , que participa de comitês de programas de uma quantidade insana de conferências… parece que atualmente são cinco ao mesmo tempo. Na BOF ao lado sobre 'CS Moderna no mundo real', também discutiram NVM e chegaram a isso completamente sozinhos. da Netcracker. Trabalhou por dez anos em desempenho e escalabilidade do NetCracker OS — software usado por operadores de telecomunicações para automatizar processos de gerenciamento de rede e equipamentos de rede. É interessado nas questões de desempenho do Java e da Oracle Database. Autor de mais de uma dezena de melhorias de desempenho no driver oficial PostgreSQL JDBC.
Transição para otimizações de baixo nível
Andrei: Você é uma figura conhecida no mundo da compilação JIT, do Java e do desempenho em geral, certo?
Cliff: Correto!
Andrei: Vamos começar com perguntas gerais sobre o trabalho em desempenho. O que você acha sobre a escolha entre otimizações de alto nível e baixo nível, como trabalhar no nível do CPU?
Cliff: Aqui tudo é simples. O código mais rápido é aquele que nunca é executado. Portanto, você sempre deve começar em um nível alto, trabalhando nos algoritmos. Uma notação O melhor deve superar uma notação O pior, a menos que intervenham algumas constantes grandes o suficiente. As coisas de baixo nível vêm por último. Geralmente, se você otimizou todo o resto da pilha bem o suficiente e ainda há algo interessante, isso é o que se chama de baixo nível. Mas como começar em um nível alto? Como saber que você fez trabalho suficiente em um nível alto? Bem... não há como. Não existem receitas prontas. Você precisa entender o problema, decidir o que pretende fazer (para não dar passos desnecessários) e então você pode pegar a ferramenta de perfilagem, que pode te dar alguma informação útil. Chega um momento em que você mesmo percebe que se livrou de coisas desnecessárias e chegou a hora de se concentrar nos ajustes finos de baixo nível. Isso é, sem dúvida, uma forma especial de arte. Muitas pessoas fazem coisas desnecessárias, mas se movimentam tão rapidamente que não têm tempo para se preocupar com o desempenho. Mas isso dura até que a questão se torne crítica. Normalmente, 99% do tempo ninguém se interessa pelo que estou fazendo, até o momento em que algo importante surge no caminho crítico, algo com que alguém se importa. E é aí que todos começam a questioná-lo sobre
Como fazer um grande refatoramento
Andrei: Como você trabalha no desempenho? Essa é uma questão abrangente. Por exemplo, você já teve que lidar com problemas que surgem da sobreposição de muitas funcionalidades já existentes?
Cliff: Eu tento evitar isso. Se eu sei que o desempenho será um problema, começo a refletir sobre isso antes de começar a codificar, especialmente sobre as estruturas de dados. Mas muitas vezes você descobre tudo isso muito mais tarde. E então você tem que tomar medidas drásticas e fazer o que eu chamo de 'reescrever e dominar': é preciso pegar uma parte suficientemente grande. Parte do código terá que ser reescrita devido a problemas de desempenho ou por algum outro motivo. Qualquer que seja a razão para reescrever o código, quase sempre é melhor reescrever uma parte maior do que uma parte menor. Neste momento, todos começam a ficar apavorados: 'oh Deus, não podemos tocar em tanto código!'. Mas, na verdade, essa abordagem quase sempre funciona muito melhor. É preciso enfrentar um grande problema, desenhar um grande círculo ao seu redor e dizer: tudo o que estiver dentro do círculo, eu vou reescrever. O limite é muito menor do que o conteúdo dentro dele que deve ser substituído. E se essa demarcação permitir que o trabalho interno seja feito perfeitamente – você tem liberdade, faça o que quiser. Assim que você entende o problema, o processo de reescrita se torna muito mais fácil, então morda um pedaço grande!
Ao mesmo tempo, quando você faz a reescrita de um grande pedaço e percebe que o desempenho se tornará um problema, pode começar a se preocupar com isso imediatamente. Normalmente, isso se transforma em coisas simples como 'não copie os dados, gerencie os dados da forma mais simples possível, faça-os menores'. Em reescritas grandes, há maneiras padrão de melhorar o desempenho. E elas quase sempre giram em torno dos dados.
Modelo de custo
Andrei: Em um dos podcasts, você falou sobre modelos de custo no contexto de desempenho. Você pode explicar o que quis dizer com isso?
Cliff: Claro. Eu nasci em uma época em que o desempenho do processador era extremamente importante. E essa era está retornando novamente – o destino não é desprovido de ironia. Eu comecei a viver na época das máquinas de oito bits, meu primeiro computador funcionava com 256 bytes. Isso mesmo, bytes. Tudo era muito pequeno. Era necessário contar instruções e, assim que começamos a avançar nas pilhas de linguagens de programação, as linguagens assumiram cada vez mais responsabilidades. Havia Assembly, depois Basic, depois C, e o C lidava com muitos detalhes, como a alocação de registradores e a otimização de instruções. Mas tudo era bastante claro, e se eu fizesse um ponteiro para uma instância de variável, teria um load, e o custo dessa instrução é conhecido. O hardware fornece um número conhecido de ciclos de máquina, então a velocidade de execução de diferentes tarefas pode ser calculada apenas somando todas as instruções que você planejou executar. Cada compare/test/branch/call/load/store podia ser somado e você poderia dizer: aqui está o seu tempo de execução. Ao melhorar o desempenho, você certamente notará quais números correspondem a pequenos ciclos quentes.
Mas assim que você muda para Java, Python e coisas parecidas, você rapidamente se afasta do hardware de baixo nível. Qual é o custo de chamar um getter em Java? Se o JIT no HotSpot fez tudo certo , isso será um load, mas, se ele não fez – será uma chamada de função. Como a chamada está em um ciclo quente, ela anulará todas as outras otimizações nesse ciclo. Portanto, o custo real será muito maior. E você imediatamente perde a capacidade de olhar para um pedaço de código e entender que devemos executá-lo em termos de frequência do processador, memória utilizada e cache. Tudo isso se torna interessante apenas se realmente você se aprofundar no desempenho.
Atualmente, nos encontramos 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ê já não pode contar com um bom desempenho em um único núcleo. Mas se você decidir realizar cálculos paralelos - isso é extremamente complicado, todos olham para você como se você fosse James Bond. Acelerações de dez vezes geralmente acontecem onde alguém deixou passar alguma coisa. A paralelidade exige muito trabalho. Para obter essa aceleração de dez vezes, é preciso entender o modelo de custo. O que e quanto custa. E para isso, é preciso entender como a linguagem se relaciona com o hardware subjacente.
Martin Thompson encontrou uma ótima palavra para seu blog ! É necessário entender o que o hardware está prestes a fazer, como exatamente ele fará isso e por que ele faz o que faz. Usando isso, é bem simples começar a contar instruções e descobrir para onde o tempo de execução está indo. Se você não tem a preparação adequada, você está apenas procurando um gato preto em um quarto escuro. Eu vejo constantemente pessoas otimizando desempenho, que não têm a menor ideia do que estão fazendo. Elas ficam muito frustradas e não conseguem avançar. E quando eu pego o mesmo pedaço de código, insiro alguns pequenos truques e obtenho um aumento de cinco ou dez vezes, elas dizem: bem, isso não é justo, nós já sabíamos que você era melhor. Impressionante. Sobre o que eu estava falando… o modelo de custo é sobre o código que você escreve e quão rápido ele, em média, funciona no quadro geral.
Andrei: E como manter essa quantidade em mente? Isso é alcançado com uma grande quantidade de experiência, certo? Onde se pode adquirir tal experiência?
Cliff: Bem, minha experiência não foi adquirida da maneira mais simples. Eu programei em Assembly na época em que era possível entender cada instrução individual. Isso pode parecer bobagem, mas desde então, na minha cabeça, ficou gravado um conjunto de instruções Z80. Eu não lembro os nomes das pessoas um minuto após a conversa, mas lembro do código que escrevi há 40 anos. Engraçado, isso parece uma forma de síndrome de '».
Aprendizado sobre otimizações de baixo nível
Andrei: Existe um caminho mais fácil para entrar nesse campo?
Cliff: Sim, e não. O hardware que todos usamos não mudou tanto ao longo do tempo. Todos utilizam x86, exceto smartphones com Arm. Se você não está envolvido com algum tipo de embedded hardcore, tem tudo igual. Certo, vamos continuar. Instruções também não mudaram ao longo dos séculos. É preciso ir e escrever algo em Assembly. Um pouco, mas o suficiente para começar a entender. Você pode estar sorrindo, mas eu falo isso muito a sério. É necessário entender a correspondência entre a linguagem e o hardware. Depois disso, é preciso ir, escrever um pouco e fazer um pequeno compilador para uma linguagem pequena e simples. 'Simples' significa que é necessário fazê-lo em um tempo razoável. Ele pode ser ultra simples, mas deve gerar instruções. O ato de gerar instruções permitirá entender o modelo de custo para a ponte entre o código de alto nível, que todos escrevem, e o código de máquina, que é executado no hardware. Essa correspondência ficará gravada nas mentes no momento em que se escreve o compilador. Mesmo um compilador bem simples. Depois disso, você pode começar a olhar para Java e perceber que sua lacuna semântica é muito mais profunda, e construir pontes sobre isso é muito mais difícil. Em Java, é muito mais difícil entender se nossa ponte é boa ou ruim, o que fará com que ela desmorone e o que não fará. Mas você precisa de algum ponto de partida, quando olha para o código e entende: 'ah, este getter deve ser inlinado toda vez'. E então você percebe que, às vezes, isso acontece, exceto quando o método se torna muito grande, e o JIT começa a inlinar tudo. O desempenho desses lugares pode ser previsto instantaneamente. Normalmente, getters funcionam bem, mas depois você olha para grandes ciclos quentes e percebe que há algumas chamadas de funções ali, e não está claro o que elas fazem. Esse é o problema com o uso excessivo de getters, a razão pela qual eles não são inlinados - não está claro se é um getter. Se você tem uma base de código super pequena, pode simplesmente memorizá-la e depois 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, na verdade, ninguém conhece. O profiler diz que perdemos 24% do tempo em algum ciclo, e para entender o que esse ciclo faz, você precisa olhar para cada função dentro. É impossível entender isso sem estudar a função, e isso desacelera seriamente o processo de compreensão. Por isso não uso getters e setters; eu avancei para um novo nível!
De onde pegar o modelo de custos? Bem, é possível ler algo, claro... Mas eu acho que a melhor maneira é agir. Fazer um pequeno compilador é a melhor forma de compreender o modelo de custos e assimilá-lo na sua própria mente. Um pequeno compilador que funcionasse para programar um micro-ondas é um desafio para iniciantes. Quero dizer, se você já tem habilidades de programação, isso deve ser suficiente. Todas essas coisas, como analisar uma string que será alguma expressão algébrica, extrair de lá as instruções das operações matemáticas na ordem correta, obter os valores corretos dos registradores - tudo isso é feito rapidamente. E enquanto você faz isso, fica gravado na sua mente. Acho que todos sabem o que um compilador faz. E isso proporcionará uma compreensão do modelo de custos.
Exemplos práticos de melhoria de desempenho
Andrei: Em que mais devemos prestar atenção ao trabalhar na performance?
Cliff: Estruturas de dados. Aliás, já faz um bom tempo que não dou essas aulas... . Isso foi divertido, mas exigiu tanto esforço, e eu ainda tenho a minha vida! Ok. Assim, em uma das grandes e interessantes aulas, "Para onde vai seu desempenho", eu dei aos alunos um exemplo: dois terabytes e meio de dados fintech lidos de um arquivo CSV, e depois era preciso calcular a quantidade de produtos vendidos. Dados de mercado normais em ticks. Pacotes UDP transformados em formato de texto desde os anos 70. Chicago Mercantile Exchange - coisas como petróleo, milho, soja, e por aí vai. Era necessário contar esses produtos, o número de transações, o volume médio de movimento de fundos e mercadorias, e assim por diante. Matemática comercial bastante simples: encontrar o código do produto (que são 1-2 caracteres em uma tabela hash), obter a soma, adicioná-la a um dos conjuntos de transações, adicionar o volume, adicionar o preço, e mais algumas outras coisas. Matemática muito simples. A implementação protótipo era muito direta: tudo estava em um arquivo, eu lia o arquivo e andava por ele, separando os registros individuais em strings Java, procurando as coisas necessárias e somando de acordo com a matemática mencionada acima. E isso funcionava com uma velocidade razoável.
Com essa abordagem, está claro o que está acontecendo, e os cálculos paralelos não ajudarão, certo? Descobriu-se que um aumento de cinco vezes no desempenho pode ser alcançado simplesmente escolhendo as estruturas de dados corretas. E isso surpreende até os programadores mais experientes! No meu caso específico, o foco estava em não fazer alocações de memória no loop crítico. Bem, essa não é toda a verdade, mas, no geral, não se deve alocar "uma vez a cada X", quando X é suficientemente grande. Quando X é de dois a meio gigabytes, não se deve alocar nada "uma vez por caractere", ou "uma vez por linha", ou "uma vez por campo", nada desse tipo. É isso que consome tempo. Como isso funciona na prática? Imagine que estou fazendo uma chamada String.split() ou BufferedReader.readLine(). Readline e transforma a string de um conjunto de bytes recebidos pela rede, uma vez para cada linha, para cada uma das centenas de milhões de linhas. Eu pego essa string, analiso e descarto. Por que descarto? Bem, eu já a processei, então, é isso. Portanto, para cada byte lido desses 2.7G, serão gravados dois caracteres na string, ou seja, já 5.4G, e eu não preciso mais deles, por isso são descartados. Se olharmos para a largura de banda da memória, estamos carregando 2.7G, que passam pela memória e pelo barramento de memória no processador, e, em seguida, o dobro é enviado para a string que está na memória, e tudo isso é manipulado na criação de cada nova string. Mas eu preciso lê-la, o hardware a lê, mesmo que depois tudo seja processado. E eu devo gravá-la, porque criei uma string e os caches ficaram cheios - o cache não pode conter 2.7G. Portanto, para cada byte lido, estou lendo mais dois bytes adicionais e escrevendo dois bytes adicionais, e no final, temos uma relação de 4:1 - assim estamos desperdiçando a largura de banda da memória. E então, descobre-se que se eu fizer String.split() – eu certamente não farei isso pela última vez, pois pode haver mais 6-7 campos por dentro. Portanto, o código clássico de leitura de CSV seguido do parsing das strings leva a perdas de largura de banda da memória na faixa de 14:1 em relação ao que você realmente gostaria de ter. Se descartarmos essas alocações, podemos obter um aumento de cinco vezes.
E não é que seja muito difícil. Se você olhar para o código do ângulo certo, tudo isso se torna bastante simples, assim que você percebe a essência do problema. Não deve parar de alocar memória: o problema é que você está alocando algo e ele morre imediatamente, queimando um recurso importante pelo caminho, que neste caso é a largura de banda da memória. E tudo isso resulta na queda de desempenho. No x86, normalmente é necessário queimar ciclos de CPU ativamente, mas aqui você queimou toda a memória muito antes. A solução é reduzir o número de alocações.
Outra parte do problema é que, se você executar o perfilador quando a largura de banda da memória se esgotou, exatamente no momento em que isso acontece, você costuma esperar que o cache retorne, porque ele está cheio de lixo que você acabou de gerar com todas essas alocações. Portanto, cada operação de load ou store se torna lenta, pois elas resultam em falhas no cache – todo o cache ficou lento, esperando que o lixo saísse dele. Assim, o perfilador apenas mostrará um ruído aleatório quente, espalhado ao longo de todo o ciclo – não haverá uma instrução ou local quente no código. Apenas ruído. E se você olhar para os ciclos de GC, todos eles estarão na Young Generation e super rápidos – microssegundos ou milissegundos no máximo. Porque toda essa memória morre instantaneamente. Você aloca bilhões de gigabytes, e ele os corta, e corta, e corta de novo. Tudo isso acontece muito rapidamente. Assim, temos ciclos de GC baratos, ruído quente ao longo de todo o ciclo, mas queremos obter uma aceleração de 5 vezes. Nesse momento, algo deve se conectar em sua cabeça e soar: 'por que isso?!'. O estouro da largura de banda da memória não é exibido no depurador clássico, você precisa iniciar o depurador de contadores de desempenho de hardware e ver isso por conta própria e diretamente. E não diretamente isso pode ser suspeitado a partir desses três sintomas. O terceiro sintoma é quando você olha o que está alocando, pergunta ao perfilador, e ele responde: 'Você fez um bilhão de alocações, mas o GC trabalhou de graça'. Assim que isso acontece, você entende que criou objetos demais e queimou toda a largura de banda da memória. Existe uma maneira de resolver isso, mas não é óbvia.
O problema está na estrutura dos dados: a estrutura bruta por trás de tudo isso é muito grande, são 2,7 GB no disco, portanto, fazer uma cópia dessa coisa é extremamente indesejável - queremos carregá-la diretamente do buffer de bytes da rede para os registradores, sem ler e escrever na linha de um lado para o outro cinco vezes. Infelizmente, o Java, por padrão, não oferece tal biblioteca no JDK. Mas isso é trivial, não é? Basicamente, são de 5 a 10 linhas de código que implementarão o seu próprio carregador de linhas em buffer, que replicará o comportamento da classe String, funcionando como um wrapper ao redor do buffer de bytes subjacente. Como resultado, você acaba trabalhando quase como se estivesse lidando com strings, mas, na verdade, os ponteiros estão se movendo para o buffer, e os bytes brutos não são copiados em lugar nenhum, assim, os mesmos buffers são reutilizados repetidamente, enquanto o sistema operacional felizmente assume tarefas para as quais foi projetado, como a dupla bufferização oculta desses buffers de bytes, e você não fica mais processando um fluxo infinito de dados desnecessários. A propósito, você sabe que ao trabalhar com GC garante-se que cada alocação de memória não será visível para o processador após o último ciclo de GC? Portanto, nada disso pode estar no cache, e então ocorre uma falha garantida de 100%. Quando se trata de um ponteiro, na arquitetura x86, ler um registrador da memória leva de 1 a 2 ciclos, e assim que isso acontece, você paga, paga, paga, porque toda a memória está – e essa é a verdadeira custo da alocação de memória. O custo real.
Em outras palavras, estruturas de dados são a parte mais difícil de mudar. E assim que você percebe que escolheu a estrutura de dados errada, que acabará comprometendo a performance, geralmente é preciso fazer um trabalho substancial. Se isso não for feito, a situação só tende a piorar. Primeiramente, é preciso pensar nas estruturas de dados, isso é crucial. O maior custo está nas estruturas de dados pesadas, que começam a ser usadas no estilo "copiarei a estrutura de dados X para a estrutura de dados Y, porque Y me agrada mais visualmente". Mas a operação de cópia (que parece barata) na verdade consome largura de banda de memória e aqui está enterrado todo o tempo de execução perdido. Se eu tiver uma enorme string JSON e quiser transformá-la em uma árvore DOM estruturada de POJO ou algo assim, a operação de análise dessa string e a construção do POJO, e depois o novo acesso ao POJO, se transformarão em um custo adicional – não é barato. Exceto se você acessar o POJO muito mais frequentemente do que a string. De forma prática, pode-se tentar decifrar a string e extrair apenas o que é necessário, sem convertê-la em POJO. Se tudo isso estiver acontecendo em um percurso que requer máxima performance, nada de POJO – é preciso mexer diretamente na string.
Por que criar sua própria linguagem de programação
Andrei: Você disse que para entender o modelo de custo, é necessário escrever uma pequena linguagem...
Cliff: Não é uma linguagem, mas um compilador. Linguagem e compilador são coisas diferentes. A principal diferença está na sua cabeça.
Andrei: A propósito, pelo que sei, você está experimentando com a criação de suas próprias linguagens. Por quê?
Cliff: Porque posso! Estou semi-aposentado, então isso é meu hobby. Passei a vida realizando os idiomas de outras pessoas. Também trabalhei muito no estilo de codificação. E porque vejo problemas em outras linguagens. Vejo que existem maneiras melhores de fazer coisas comuns. E eu gostaria de usá-las. Estou apenas cansado de ver problemas em mim mesmo, em Java, em Python, em qualquer outra linguagem. Atualmente estou escrevendo em 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 com aprendizado de máquina para backends Java. Existem muitas linguagens populares, todas com características interessantes. Cada uma é boa por algo específico e é possível tentar unir todas essas funcionalidades. Então, estou explorando coisas que me interessam, o comportamento da linguagem, tentando inventar uma semântica razoável. E até agora, está funcionando! No momento, estou lidando com a semântica de memória, porque quero tê-la como em C e Java, e obter um modelo de memória forte e a semântica de memória para cargas e armazenamentos. Ao mesmo tempo, ter inferência de tipos automática como em Haskell. Aqui, estou tentando misturar a inferência de tipos semelhante a Haskell com uma memória que funcione como em C e Java. É isso que venho fazendo nos últimos 2-3 meses, por exemplo.
Andrei: Se você está construindo uma linguagem que extrai os melhores aspectos de outras linguagens, você já pensou que alguém poderia fazer o contrário: pegar suas ideias e utilizá-las em seu próprio projeto?
Cliff: É assim que novas linguagens surgem! Por que Java é semelhante a C? Porque C tinha uma boa sintaxe que todos entendiam e Java se inspirou nessa sintaxe, adicionando segurança de tipos, verificações de limites de arrays, GC, e também melhoraram algumas coisas de C. Mas eles se inspiraram bastante, certo? Todos estamos em cima dos ombros de gigantes que vieram antes de nós – é assim que se faz progresso.
Andrei: Pelo que entendi, sua linguagem será segura em relação ao uso de memória. Você já considerou implementar algo como o borrow checker de Rust? Você já olhou para ele, o que achou?
Cliff: Bem, eu estou programando em C há uma eternidade, com todas essas chamadas de malloc e free, e gerenciando manualmente o tempo de vida. Sabe, 90-95% do tempo de vida gerenciado manualmente tem uma estrutura semelhante. E é muito, muito doloroso lidar com isso manualmente. Gostaria que o compilador apenas informasse o que está acontecendo e o que você alcançou com suas ações. Para algumas coisas, o borrow checker faz isso por padrão. E ele também deve automaticamente fornecer informações, entender tudo e nem me sobrecarregar com o que é necessário para entender. Ele deve realizar, no mínimo, uma análise de escape local, e apenas se isso falhar é que seria necessário adicionar anotações de tipo que descrevam o tempo de vida – e esse tipo de esquema é muito mais complicado do que o borrow checker, ou qualquer checker de memória existente. A escolha entre 'tudo certo' e 'não entendi nada' — não, deveria haver algo melhor.
Assim, como alguém que escreveu muito código em C, acredito que ter suporte para gerenciamento automático de lifetime é a coisa mais importante. Além disso, estou cansado de como Java utiliza memória e a principal reclamação é sobre o GC. Quando se aloca memória em Java, você não recebe de volta a memória que foi local na última execução do ciclo GC. Em linguagens com gerenciamento de memória mais preciso, isso não acontece. Se você chama malloc, recebe imediatamente uma memória que geralmente foi utilizada há pouco. Normalmente, você faz alguns trabalhos temporários com a memória e a devolve imediatamente. E ela retorna ao pool de malloc, e o próximo ciclo de malloc a utiliza novamente. Portanto, o uso real de memória é reduzido ao conjunto de objetos vivos em um determinado momento, mais as vazamentos. E se não haver vazamentos de forma vergonhosa, a maior parte da memória se instala em caches e no processador, e isso funciona rapidamente. Mas requer muito gerenciamento manual de memória usando malloc e free, chamadas na ordem correta, no local certo. Rust consegue cuidar disso por si só e, em muitos casos, pode até fornecer um desempenho maior, pois o consumo de memória se reduz apenas para os cálculos atuais - ao contrário da expectativa de um próximo ciclo de GC que liberará a memória. No final, conseguimos uma maneira muito interessante de melhorar o desempenho. E bastante poderosa - quero dizer, trabalhava com coisas assim no processamento de dados para fintech, e isso permitia um aumento de desempenho de aproximadamente cinco vezes. Isso é um aumento considerável, especialmente em um mundo onde os processadores não ficam mais rápidos, enquanto ainda esperamos melhorias.
Carreira de engenheiro de desempenho
Andrei: Também gostaria de perguntar sobre a carreira como um todo. Você se tornou famoso por seu trabalho no JIT no HotSpot, e depois se transferiu para a Azul - que também é uma empresa de JVM. Mas você passou a trabalhar mais com hardware do que com software. E então, de repente, se mudou para Big Data e Machine Learning, e depois para detecção de fraudes. Como isso aconteceu? Estas são áreas de desenvolvimento muito diferentes.
Cliff: Eu já trabalho com programação há bastante tempo e consegui me destacar em diversas atividades. Quando as pessoas dizem: 'oh, você é aquele que fez o JIT para Java!', isso sempre me faz rir. Antes disso, trabalhei em um clone do PostScript – a linguagem que a Apple usava para suas impressoras a laser. Antes disso, implementei a linguagem Forth. Acredito que o tema central para mim sempre foi o desenvolvimento de ferramentas. Passei a vida criando ferramentas que permitem que outras pessoas escrevam seus programas incríveis. Mas também trabalhei no desenvolvimento de sistemas operacionais, drivers, depuradores de nível de núcleo e linguagens para desenvolvimento de SOs, que começaram simples, mas foram se tornando cada vez mais complexas. Contudo, o foco principal, ainda assim, é a criação de ferramentas. Uma parte significativa da minha vida se passou entre a Azul e a Sun, e foi dedicada ao Java. Mas quando comecei a trabalhar com Big Data e Machine Learning, coloquei meu chapéu de festa e disse: 'Oh, agora temos um problema não trivial, e muitas coisas interessantes estão acontecendo com pessoas que estão fazendo algo'. Este é um excelente caminho para o desenvolvimento que vale a pena seguir.
Sim, eu realmente amo computação distribuída. Meu primeiro trabalho foi durante a faculdade, em C, em um projeto publicitário. Era computação distribuída em chips Zilog Z80, que coletavam dados para reconhecimento óptico analógico de textos, realizado por um verdadeiro analisador analógico. Era um tema incrível e totalmente fora do comum. Mas havia problemas, algumas partes não eram reconhecidas corretamente, então precisávamos extrair a imagem e mostrá-la a uma pessoa que pudesse ler e informar o que estava escrito, e assim havia jobs de dados, e esses jobs tinham sua própria linguagem. Havia um backend que processava tudo isso – Z80s funcionando em paralelo com terminais vt100 – um para cada pessoa, e havia um modelo de programação paralela em Z80. Um pedaço comum de memória que era compartilhado por todos os Z80 dentro de uma configuração em estrela; o backplane era compartilhado, e metade da RAM era compartilhada dentro da rede, enquanto a outra metade era privada ou usada para outra coisa. Uma sistema paralelo distribuído complexamente significativo com memória compartilhada… semi-compartilhada. Quando foi isso… Já não me lembro, em algum lugar no meio dos anos 80. Faz bastante tempo.
Sim, vamos considerar que 30 anos é bastante tempo. As tarefas relacionadas à computação distribuída existem há muito tempo, as pessoas têm lutado com -clusters. Esses clusters parecem como… Por exemplo: há Ethernet e seu rápido x86 conectado a esse Ethernet, e agora você deseja obter uma fake shared memory, porque ninguém poderia então trabalhar na programação de computação distribuída, isso era muito complexo e, por isso, havia uma fake shared memory com proteção de páginas de memória em x86, e se você gravasse nessa página, nós informávamos aos outros processadores que, se eles acessassem a mesma shared memory, precisariam carregá-la de você, e assim surgiu algo parecido com um protocolo de suporte à coerência de cache e software para isso. Uma ideia interessante. O verdadeiro problema, é claro, estava em outro lugar. Tudo isso funcionava, mas você rapidamente enfrentava problemas de desempenho, pois ninguém entendia o modelo de desempenho em um nível suficientemente bom – quais eram os padrões de acesso à memória, como fazer com que os nós não se pingssem interminavelmente, e assim por diante.
No H2O, eu concebi o seguinte: os próprios desenvolvedores são responsáveis por identificar onde está o paralelismo e onde não está. Eu criei um modelo de codificação que torna fácil e simples escrever código de alto desempenho. Escrever código que funcione lentamente é difícil; ele será mal estruturado. É necessário esforço considerável para escrever código lento, e métodos não convencionais serão necessários. Código que desacelera é visível à primeira vista. Como resultado, normalmente se escreve código que funciona rapidamente, mas você precisa descobrir o que fazer em caso de memória compartilhada. Tudo isso está atrelado a grandes arrays, e o comportamento é semelhante aos arrays grandes não voláteis em Java paralelo. Ou seja, imagine que dois threads escrevem em um array paralelo, um deles vence, e o outro, por consequência, perde, e você não sabe quem é quem. Se eles não forem voláteis, a ordem pode ser qualquer uma – e isso realmente funciona bem. As pessoas se preocupam com a ordem das operações, elas corretamente estabelecem o volatile e, nos locais certos, antecipam problemas de desempenho relacionados à memória. Caso contrário, elas simplesmente escreveriam código em loops de 1 a N, onde N são trilhões, na esperança de que todos os casos complexos se tornassem automaticamente paralelos – e isso não funciona. Mas no H2O, isso não é Java, nem Scala; pode-se considerar como um 'Java menos menos', se quiser. É um estilo de programação muito compreensível e se assemelha à escrita de código simples em C ou Java com loops e arrays. Contudo, a memória pode ser manipulada em terabytes. Eu ainda uso o H2O. O uso é ocasional em diferentes projetos – e até hoje, é a coisa mais rápida, estando dezenas de vezes à frente dos concorrentes. Se você está lidando com Big Data e dados em colunas, é muito difícil superar o H2O.
Desafios técnicos
Andrei: Qual foi o maior desafio que você enfrentou em toda a sua carreira?
Cliff: Estamos discutindo a parte técnica ou a não técnica da questão? Eu diria que os maiores desafios são os não técnicos.
Quanto aos desafios técnicos. Eu simplesmente os superei. Nem mesmo sei qual foi o maior, mas houve vários bastante interessantes que consumiram bastante tempo e luta mental. Quando fui para a Sun, estava certo de que faria um compilador rápido, enquanto um monte de seniores dizia que eu nunca conseguiria nada. Mas segui esse caminho, escrevi um compilador até o alocador de registradores, e bastante rápido. Ele era tão rápido quanto o moderno C1, mas na época o alocador era muito mais lento, e olhando para trás – foi um problema de grandes estruturas de dados. Eu precisava disso para escrever um alocador gráfico de registradores e não entendia a dilema entre a expressividade do código e a velocidade, que existia naquela época e era muito importante. Descobri que a estrutura de dados geralmente superava o tamanho do cache nos x86 da época, e portanto, se eu inicialmente supus que o alocador de registradores levaria de 5 a 10 por cento de todo o tempo de JIT, na realidade isso se traduziu em 50 por cento.
O tempo passava, o compilador tornava-se cada vez mais claro e eficiente, parou de gerar código horrível na maioria dos casos, e o desempenho começou a se assemelhar ao que um compilador C produce. Se você, é claro, não está escrevendo alguma porcaria que nem mesmo C pode otimizar. Se você escreve código como em C, obtém desempenho similar ao de C na maioria dos casos. E quanto mais avançávamos, mais frequentemente resultava em código que coincidencialmente se igualava ao nível de C, o alocador de registradores começou a se assemelhar a algo completo... independentemente de seu código estar rápido ou devagar. Eu continuei a trabalhar no alocador para que ele fizesse alocações melhores. Ele estava ficando mais lento, mas produzia desempenhos cada vez melhores em casos onde ninguém mais conseguia. Eu podia mergulhar no alocador de registradores, enterrar um mês de trabalho lá, e de repente todo o código começava a rodar 5% mais rápido. Isso acontecia repetidamente, e o alocador de registradores se tornou uma espécie de obra de arte - todos o amavam ou o odiavam, e as pessoas da academia faziam perguntas sobre o porquê de tudo ser feito daquela forma, por que não , e qual é a diferença. A resposta continua a mesma: o alocador baseado em coloração de grafos mais um trabalho muito cuidadoso com o código de buffer resulta em uma ferramenta vencedora, a melhor combinação que ninguém consegue superar. E isso é algo bastante não óbvio. Tudo o mais que o compilador faz são coisas bastante conhecidas, embora também tenham sido elevadas ao nível da arte. Sempre fiz coisas que deveriam transformar o compilador em uma obra de arte. Mas nada disso era algo extraordinário – exceto o alocador de registros. O foco é que é preciso lidar com cuidado sob carga e, se isso acontecer (eu posso explicar em mais detalhes, se estiver interessado), isso significa que é possível fazer inlining de forma mais agressiva, sem o risco de ultrapassar o ponto de inflexão da performance. Naquela época, havia uma infinidade de compiladores de grande escala, repletos de firulas e apetrechos, que tinham alocadores de registros, mas ninguém mais conseguiu isso.
O problema é que, se você adicionar métodos que devem ser inlined, aumentando e aumentando a área de inlining, o conjunto de valores utilizados ultrapassa instantaneamente o número de registros, e você precisa fazer spill. O nível crítico geralmente ocorre quando o alocador falha, e um bom candidato para spill vale mais do que o outro, você faz spill de algumas coisas realmente absurdas. O valor do inlining aqui está no fato de que você perde parte da sobrecarga, a sobrecarga na chamada e na preservação, você pode ver os valores internamente e pode otimizá-los ainda mais. O custo do inlining é que se forma uma grande quantidade de valores vivos, e se seu alocador de registros faz spill de mais do que o necessário, você imediatamente perde. Portanto, a maioria dos alocadores enfrenta um problema: quando o inlining ultrapassa um certo limite, tudo começa a fazer spill e o desempenho pode ser arrastado para o ralo. Aqueles que implementam um compilador adicionam algumas heurísticas: por exemplo, para parar o inlining ao atingir um tamanho grande o suficiente, pois as alocações estragam tudo. Surge assim uma quebra na curva de desempenho - você inlining, inlining, o desempenho aumenta lentamente - e então, bum! - ele despenca com um colapso rápido, porque você inlinou demais. Tudo funcionou assim até a chegada do Java. O Java requer muito mais inlining, então tive que fazer meu alocador muito mais agressivo para que ele se equilibrasse em vez de cair, e se você inlinou demais - ele começa a fazer spill, mas ainda assim chega um momento em que "não há mais spill". Essa é uma observação interessante que me veio do nada, não óbvia, mas muito recompensadora. Eu me envolvi com inlining agressivo e isso me levou a lugares onde o desempenho do Java e do C estão lado a lado. Eles são realmente próximos - posso escrever código em Java que será significativamente mais rápido do que o código em C e coisas assim, mas em média, na grande visão das coisas, eles são aproximadamente comparáveis. Acredito que parte deste mérito se deve ao alocador de registros, que me permite inlining de maneira muito simples. Eu simplesmente inline tudo que vejo. A questão é se o alocador funciona bem, se o código resultante é razoavelmente eficiente. Esse foi um grande desafio: entender tudo isso e fazer funcionar.
Um pouco sobre alocação de registradores e multiprocessamento
Vladimir: Problemas como a alocação de registradores parecem um tema eterno e interminável. É interessante saber se alguma ideia parecia promissora e, depois, falhou na prática?
Cliff: Certamente! A alocação de registradores é uma área em que, para resolver um problema NP-completo, você tenta encontrar algumas heurísticas. E você nunca conseguirá uma solução perfeita, certo? Isso simplesmente é impossível. Veja, a compilação Ahead of Time também não funciona bem. A conversa aqui gira em torno de casos médios. Sobre a performance típica, de modo que você pode ir e medir algo que considera uma boa performance típica - afinal, você está trabalhando para melhorá-la! A alocação de registradores é um tema inteiramente dedicado à performance. Assim que você tem o primeiro protótipo, ele funciona e realiza o que precisa, começa o trabalho de performance. É preciso aprender a medir bem. Por que isso é importante? Se houver dados claros, você pode olhar para diferentes partes e ver: ah, isso ajudou aqui, mas ali tudo quebrou! Surgem boas ideias, você adiciona uma nova heurística e, de repente, tudo começa a funcionar um pouco melhor em média. Ou não começa. Tive várias situações em que lutamos por cinco por cento de performance, que distinguiam nosso desenvolvimento do alocador anterior. E cada vez é assim: em algum lugar você ganhou, em algum lugar você perdeu. Se você tiver boas ferramentas de análise de performance, pode encontrar ideias perdedoras e entender por que elas falharam. Talvez seja melhor manter tudo como está, ou talvez seja hora de se dedicar mais à afinação, ou ir e corrigir outra coisa. É um conjunto inteiro de coisas! Eu fiz esse hack incrível, mas preciso disso, e disso, e disso – e a combinação total delas traz algumas melhorias. Ideias isoladas podem falhar. Essa é a natureza do trabalho em tarefas NP-completas relacionadas à performance.
Vladimir: Tem a impressão de que tarefas como pintura em alocadores é um problema já resolvido. Bem, para você, parece resolvido, considerando o que você relata, então vale a pena…
Cliff: Não está resolvido como tal. Você deve transformá-la em "resolvida". Existem problemas difíceis que precisam ser resolvidos. Quando isso é feito, chega a hora de trabalhar na performance. É preciso levar esse trabalho a sério - fazer benchmarks, coletar métricas, explicar situações quando, ao voltar para uma versão anterior, seu antigo hack começou a funcionar novamente (ou vice-versa, parou de funcionar). E não desista até conseguir algo. Como já mencionei, se há ideias legais que não funcionaram, na área de alocação de registradores, as ideias são praticamente infinitas. Você pode, por exemplo, ler publicações científicas. Embora agora essa área tenha avançado muito mais devagar e tenha se tornado mais clara do que em seus primórdios. No entanto, nesse campo trabalha uma infinidade de pessoas e todas as suas ideias merecem ser testadas, todas aguardando sua oportunidade. E você não pode dizer quão boas elas são se não as experimentar. Quão bem elas se integram com o resto do seu alocador, pois o alocador faz muitas coisas, e algumas ideias podem não funcionar muito bem no seu alocador específico, enquanto em outro alocador funcionariam perfeitamente. O principal modo de vitória para o alocador é retirar as coisas lentas do caminho principal e forçar a divisão nas fronteiras dos caminhos lentos. Portanto, se você quer iniciar o GC, seguir pelo caminho lento, desotimizar, lançar exceções, tudo nesse sentido - você sabe que essas coisas são relativamente raras. E elas realmente são raras, eu cheguei a verificar. Você faz trabalho adicional e, graças a isso, muitas limitações nesses caminhos lentos desaparecem, mas isso não é muito importante, porque eles são lentos e raramente utilizados. Por exemplo, um ponteiro nulo - isso nunca acontece, certo? É preciso ter vários caminhos para diferentes situações, mas eles não devem se sobrepor no principal.
Vladimir: O que você pensa sobre a multicore, quando você tem milhares de núcleos de uma vez? Isso é algo útil?
Cliff: O sucesso das GPUs mostra que é bastante útil!
Vladimir: Elas são bastante especializadas. E quanto aos processadores de uso geral?
Cliff: Bem, esse era o modelo de negócios da Azul. A resposta veio em uma era em que as pessoas realmente apreciavam desempenho previsível. Naquela época, era um desafio escrever código paralelo. O modelo de codificação H2O escala bem, mas não é um modelo de uso geral. No entanto, é um pouco mais geral do que usar uma GPU. Estamos falando sobre a complexidade de desenvolver algo assim ou sobre a complexidade de usá-lo? Por exemplo, uma lição interessante que aprendi com a Azul, bastante não óbvia: caches pequenos são aceitáveis.
O maior desafio da vida
Vladimir: E quanto aos desafios não técnicos?
Cliff: O maior desafio foi não ser... gentil e bom com as pessoas. E como consequência, eu frequentemente me encontrava em situações extremamente conflituosas. Aqueles em que eu sabia que tudo estava indo na direção errada, mas não sabia como avançar na resolução desses problemas e não conseguia lidar com eles. Muitas questões prolongadas, que duraram várias décadas, apareceram exatamente dessa maneira. O fato de que no Java existem compiladores C1 e C2 é uma consequência direta disso. O fato de que no Java por mais de uma década não houve compilação em múltiplos níveis também é uma consequência direta. É óbvio que precisávamos de um sistema assim, mas não estava claro o motivo de sua ausência. Eu tive problemas com um engenheiro... ou um grupo de engenheiros. Há muito tempo, quando comecei a trabalhar na Sun, eu era... Bem, não só naquela época, eu sempre tive minha própria opinião sobre tudo. E eu acreditava ser verdade que poderia simplesmente apresentar essa minha verdade de forma direta. Especialmente porque fiquei chocantemente certo na maior parte do tempo. E se você não gosta dessa abordagem... especialmente se você está claramente errado e fazendo bobagens... Enfim, poucas pessoas podiam suportar pacientemente esse estilo de comunicação. Embora alguns pudessem, por exemplo, eu. Eu construí minha vida inteira com base em princípios meritocráticos. Se você me mostrar algo errado, imediatamente vou me voltar e dizer: você disse uma besteira. E, claro, peço desculpas, e tudo mais, reconheço os méritos, se houver, e tomo outras ações corretas. Por outro lado, eu estou chocantemente certo em uma porcentagem chocantemente alta do tempo geral. E isso não funciona muito bem nas relações com as pessoas. Eu não estou tentando ser gentil, mas coloco a questão de forma direta. 'Isto nunca vai funcionar, porque um, dois e três'. E eles ficam: 'Oh!'. Houve outras consequências que é melhor talvez ignorar: por exemplo, resultaram em um divórcio e uma década de depressão após isso.
O desafio é lutar contra as percepções que as pessoas têm sobre o que você pode ou não fazer, o que é importante e o que não é. Houve muitos desafios sobre estilo de codificação. Eu ainda escrevo muito código, e naquelas épocas eu até precisei desacelerar, porque estava fazendo muitas tarefas paralelas e as fazia mal, em vez de me concentrar em uma só. Olhando para trás, escrevi metade do código da equipe Java JIT, da equipe C2. O próximo codificador mais rápido escrevia a metade da velocidade, o seguinte escrevia ainda mais devagar e isso era uma queda exponencial. O sétimo da fila era muito, muito lento – isso sempre acontece! Eu analisei uma tonelada de código. Observei quem estava escrevendo o quê, sem exceções, estive de olho no código deles, revisei cada um deles e ainda assim continuei escrevendo mais do que qualquer um deles. Com as pessoas, essa abordagem não funciona muito bem. Alguns não gostam disso. E quando não conseguem lidar, começam a surgir todos os tipos de reclamações. Por exemplo, uma vez me disseram para parar de escrever código, porque eu estava escrevendo código demais, e isso colocava a equipe em risco, e tudo isso soou como uma piada para mim: cara, se toda a equipe restante desaparecer e eu continuar escrevendo código, você vai perder apenas metade da equipe. Por outro lado, se eu continuar escrevendo código e você perder metade da equipe – isso soa como uma gestão muito ruim. Eu nunca pensei muito sobre isso, nunca falei sobre isso, mas estava de alguma forma na minha cabeça. Na parte de trás da minha mente, a ideia girava: 'Vocês estão brincando, não é?'. Assim, o maior problema era eu e meus relacionamentos com as pessoas. Agora, eu me entendo muito melhor; fui líder de equipe por muito tempo, e agora eu digo diretamente às pessoas: sabe, eu sou como sou, e vocês vão ter que lidar comigo – tudo bem se eu ficar aqui? E quando eles começaram a lidar com isso, tudo funcionou. Na verdade, eu não sou nem bom nem ruim, não tenho más intenções ou ambições egoístas, é apenas a minha essência, e precisamos lidar com isso de alguma forma.
Andrei: Recentemente, todos estavam falando sobre autoconsciência para introvertidos e, de maneira geral, sobre soft skills. O que se pode dizer sobre isso?
Cliff: Sim, isso foi um entendimento e uma lição que tirei do meu divórcio com minha esposa. O que extraí do divórcio foi a compreensão de mim mesmo. Assim, comecei a entender outras pessoas. Entender como essa interação funciona. Isso resultou em descobertas uma após a outra. Surgiu a consciência de quem sou e o que represento. O que faço: ou fico obcecado com a tarefa, ou evito conflitos, ou algo mais – e esse nível de autoconsciência realmente ajuda a manter o controle. Depois disso, tudo se torna muito mais fácil. Uma coisa que descobri não apenas em mim, mas também em outros programadores – é a incapacidade de verbalizar pensamentos quando se está em estado de estresse emocional. Por exemplo, você está codificando, está em um estado de fluxo, e então alguém corre até você e começa a gritar em pânico que algo quebrou, que medidas extremas serão tomadas. E você não consegue dizer uma palavra, porque está sob estresse emocional. O conhecimento adquirido permite se preparar para esse momento, passar por ele e passar para o plano de retirada, a partir do qual você pode começar a agir. Então, sim, quando você começa a perceber como tudo isso funciona – é um grande evento que muda a vida.
Eu mesmo não consegui encontrar as palavras certas, mas me lembrei da sequência de ações. A essência é que essa reação é tão física quanto verbal, e você precisa de espaço. Um espaço desse tipo, no sentido do zen. É isso que precisa ser explicado, e então se afastar imediatamente – fisicamente. Quando estou em silêncio, consigo processar a situação em termos de emoções. À medida que a adrenalina chega ao cérebro, coloca você no modo 'lutar ou fugir', você já não consegue dizer nada, não – agora você é um idiota, um engenheiro vulnerável, incapaz de uma resposta digna ou de, pelo menos, parar a agressão, e o agressor pode atacar livremente novamente e novamente. Primeiramente, é preciso se tornar novamente você mesmo, recuperar o controle, sair do modo 'lutar ou fugir'.
E é por isso que é necessário ter um espaço verbal. Apenas um espaço livre. Se for para falar de algo, é isso que se pode afirmar, e depois sair para realmente encontrar seu 'espaço': passear no parque, fechar-se no chuveiro – não importa. O importante é desconectar-se temporariamente da situação. Assim que você se desconecta, mesmo que por alguns segundos, o controle retorna, você começa a pensar de forma clara. 'Bem, eu não sou algum idiota, não faço coisas estúpidas, sou uma pessoa bastante útil'. Assim que você consegue convencer a si mesmo, é hora de passar para a próxima etapa: entender o que aconteceu. Você foi atacado, o ataque veio de onde você menos esperava, foi uma emboscada traiçoeira. Isso é ruim. O próximo passo é entender por que o atacante fez isso. Realmente, por quê? Talvez porque ele mesmo esteja furioso? Por que ele está furioso? Por exemplo, porque cometeu um erro e não consegue assumir a responsabilidade? É dessa forma que você precisa lidar cuidadosamente com toda a situação. Mas para isso, é necessário ter espaço para manobras, um espaço verbal. O primeiro passo é romper o contato verbal. Sair da discussão em palavras. Cancelá-la, sair o mais rápido possível. Se for uma conversa telefônica – simplesmente desligue – é uma habilidade que aprendi ao lidar com minha ex-esposa. Se a conversa não leva a coisa alguma boa, apenas diga 'tchau' e desligue. Do outro lado do telefone: 'blá-blá-blá', você responde: 'certo, tchau!' e desliga. Apenas interrompa a conversa. Cinco minutos depois, quando sua capacidade de pensar claramente retorna, você se acalma um pouco, e é possível refletir sobre o que aconteceu e o que virá a seguir. E começar a formular uma resposta ponderada, em vez de apenas reagir emocionalmente. Para mim, a descoberta em termos de autoconsciência foi que, em situações de estresse emocional, não consigo falar. Sair desse estado, pensar e planejar como responder e compensar os problemas - esses são os passos corretos quando você não consegue se expressar. O modo mais simples é fugir da situação que causa estresse emocional e simplesmente parar de participar desse estresse. Depois disso, você Recupera a capacidade de pensar, quando consegue pensar, surgem a possibilidade de falar e assim por diante.
A propósito, no tribunal, o advogado da parte contrária tenta fazer isso com você – agora está claro por quê. Porque ele tem a capacidade de te suprimir a ponto de você não conseguir nem pronunciar seu próprio nome, por exemplo. No sentido mais literal, você não consegue falar. Se isso está acontecendo com você e se você sabe que vai estar em um lugar onde palavras serão um campo de batalha, como um tribunal, é recomendável ir com seu advogado. O advogado irá defendê-lo e interromper a agressão verbal, e fará isso de maneira totalmente legal, permitindo que você retome seu espaço zen perdido. Por exemplo, em algumas ocasiões eu precisei ligar para minha família, o juiz foi bastante amigável, mas o advogado da parte contrária gritava e gritava comigo, e eu não conseguia nem intervir. Nesses casos, o que funciona melhor para mim é usar um intermediário. O intermediário interrompe toda essa pressão que vem sobre você de forma constante, e você redescobre o espaço zen necessário, junto com a habilidade de falar. É um campo de conhecimento onde é preciso aprender muito, descobrir muitas coisas dentro de si mesmo, e isso se transforma em decisões estratégicas de alto nível, diferentes para cada pessoa. Algumas pessoas não têm os problemas descritos acima, geralmente, elas não ocorrem em pessoas que trabalham profissionalmente com vendas. Todas essas pessoas que ganham a vida com palavras – cantores famosos, poetas, líderes religiosos e políticos, sempre têm algo a dizer. Eles não enfrentam esses problemas, mas eu enfrento.
Andrei: Isso foi… inesperado. Ótimo, já falamos bastante e é hora de encerrar essa entrevista. Certamente nos encontraremos na conferência e poderemos continuar esse diálogo. Nos vemos na Hydra!
O contato com Cliff poderá ser continuado na conferência Hydra 2019, que ocorrerá nos dias 11 e 12 de julho de 2019 em São Petersburgo. Ele virá com uma apresentação . Os ingressos podem ser adquiridos .
Fonte: habr.com
