(Destina-se principalmente àqueles que possuem conhecimentos específicos de sistemas de baixa latência, para evitar que as pessoas apenas respondam com opiniões sem fundamento).
Você sente que existe uma troca entre escrever código "bom" orientado a objeto e escrever código de baixa latência muito rápido? Por exemplo, evitando funções virtuais em C ++ / a sobrecarga do polimorfismo etc - reescrevendo código que parece desagradável, mas é muito rápido etc?
É lógico - quem se importa se parecer feio (desde que seja sustentável) - se você precisa de velocidade, precisa de velocidade?
Gostaria de ouvir pessoas que trabalharam nessas áreas.
Respostas:
Sim.
É por isso que a frase "otimização prematura" existe. Ele existe para forçar os desenvolvedores a medir seu desempenho e otimizar apenas o código que fará diferença no desempenho, enquanto projeta de maneira sensata sua arquitetura de aplicativos desde o início, para que não caia sob carga pesada.
Dessa forma, na máxima extensão possível, você mantém seu código orientado a objetos, bonito e bem arquitetado, e otimiza apenas com código feio aquelas pequenas porções que importam.
fonte
Sim, o exemplo que eu dou não é C ++ vs. Java, mas é Assembly vs. COBOL, pois é o que eu sei.
Os dois idiomas são muito rápidos, mas mesmo o COBOL, quando compilado, tem muito mais instruções que são colocadas no conjunto de instruções que não necessariamente precisam estar lá versus escrever essas instruções no Assembly.
A mesma idéia pode ser aplicada diretamente à sua questão de escrever "código de aparência feia" vs. usar herança / polimorfismo em C ++. Eu acredito que é necessário escrever um código feio, se o usuário final precisar de prazos de transação de menos de um segundo, é nosso trabalho como programadores dar a eles que não importa como isso aconteça.
Dito isto, o uso liberal de comentários aumenta muito a funcionalidade e a capacidade de manutenção do programador, não importa quão feio seja o código.
fonte
Sim, existe uma troca. Com isso, quero dizer que o código mais rápido e mais feio não é necessário melhor - os benefícios quantitativos do "código rápido" precisam ser ponderados com base na complexidade de manutenção das alterações de código necessárias para atingir essa velocidade.
O trade-off vem do custo do negócio. O código mais complexo requer programadores mais qualificados (e programadores com um conjunto de habilidades mais focado, como aqueles com arquitetura de CPU e conhecimento de design), leva mais tempo para ler e entender o código e corrigir bugs. O custo comercial do desenvolvimento e manutenção desse código pode estar entre 10 e 100 vezes mais do que o código normalmente escrito.
Esse custo de manutenção é justificável em alguns setores , nos quais os clientes estão dispostos a pagar um prêmio muito alto por software muito rápido.
Algumas otimizações de velocidade geram melhor retorno sobre o investimento (ROI) do que outras. Nomeadamente, algumas técnicas de otimização podem ser aplicadas com menor impacto na manutenção do código (preservando a estrutura de nível superior e a legibilidade de nível inferior) em comparação com o código normalmente escrito.
Assim, o proprietário de uma empresa deve:
Essas compensações são altamente específicas às circunstâncias.
Isso não pode ser decidido da melhor maneira possível sem a participação de gerentes e proprietários de produtos.
Estes são altamente específicos para plataformas. Por exemplo, CPUs de desktop e móveis têm considerações diferentes. Aplicativos de servidor e cliente também têm considerações diferentes.
Sim, geralmente é verdade que o código mais rápido parece diferente do código normalmente escrito. Qualquer código diferente levará mais tempo para ler. Se isso implica feiura está nos olhos de quem vê.
As técnicas com as quais tenho alguma exposição são: (sem tentar reivindicar nenhum nível de especialização) otimização de vetor curto (SIMD), paralelismo detalhado de tarefas, pré-alocação de memória e reutilização de objetos.
O SIMD geralmente tem impactos graves na legibilidade de baixo nível, mesmo que normalmente não exija alterações estruturais de nível superior (desde que a API seja projetada com a prevenção de gargalos em mente).
Alguns algoritmos podem ser transformados em SIMD facilmente (o vetor embaraçoso). Alguns algoritmos requerem mais reorganizações de computação para usar o SIMD. Em casos extremos, como o paralelismo SIMD da frente de onda, algoritmos inteiramente novos (e implementações patenteáveis) precisam ser escritos para tirar vantagem.
A paralelização de tarefas refinada exige a reorganização dos algoritmos nos gráficos de fluxo de dados e aplica repetidamente a decomposição funcional (computacional) ao algoritmo até que nenhum benefício adicional na margem possa ser obtido. Os estágios decompostos geralmente são encadeados com o estilo de continuação, um conceito emprestado da programação funcional.
Por decomposição funcional (computacional), algoritmos que poderiam ter sido normalmente escritos em uma sequência linear e conceitualmente clara (linhas de código que são executáveis na mesma ordem em que são escritas) precisam ser divididos em fragmentos e distribuídos em várias funções ou classes. (Veja a objetificação do algoritmo, abaixo.) Essa alteração impedirá enormemente outros programadores que não estão familiarizados com o processo de design de decomposição que deu origem a esse código.
Para tornar esse código sustentável, os autores desse código devem escrever documentações elaboradas do algoritmo - muito além do tipo de comentário de código ou diagramas UML feitos para código normalmente escrito. É semelhante à maneira como os pesquisadores escrevem seus trabalhos acadêmicos.
Não, o código rápido não precisa estar em contradição com a orientação a objetos.
Em outras palavras, é possível implementar um software muito rápido que ainda é orientado a objetos. No entanto, na extremidade inferior dessa implementação (no nível das porcas e parafusos, onde ocorre a maior parte da computação), o design do objeto pode se desviar significativamente dos designs obtidos do OOD (Oriented Object Design). O design de nível inferior é voltado para a objetificação de algoritmos.
Alguns benefícios da programação orientada a objetos (OOP), como encapsulamento, polimorfismo e composição, ainda podem ser obtidos a partir da objetificação de algoritmos de baixo nível. Essa é a principal justificativa para o uso de OOP nesse nível.
A maioria dos benefícios do design orientado a objetos (OOD) são perdidos. Mais importante ainda, não há intuitividade no design de baixo nível. Um colega programador não pode aprender a trabalhar com o código de nível inferior sem primeiro entender completamente como o algoritmo foi transformado e decomposto em primeiro lugar, e esse entendimento não é obtido com o código resultante.
fonte
Sim, às vezes o código precisa ser "feio" para fazê-lo funcionar no tempo necessário, embora todo o código não precise ser feio. O desempenho deve ser testado e analisado antes para encontrar os bits de código que precisam ser "feios" e essas seções devem ser anotadas com um comentário para que futuros desenvolvedores saibam o que é propositadamente feio e o que é apenas preguiça. Se alguém estiver escrevendo muitos códigos mal projetados, alegando motivos de desempenho, faça-o provar.
A velocidade é tão importante quanto qualquer outro requisito de um programa, dar correções erradas a um míssil guiado é equivalente a fornecer as correções corretas após o impacto. A manutenção é sempre uma preocupação secundária ao código de trabalho.
fonte
Alguns dos estudos dos quais vi extratos indicam que o código limpo e fácil de ler geralmente é mais rápido do que o código mais complexo e difícil de ler. Em parte, isso se deve à maneira como os otimizadores são projetados. Eles tendem a ser muito melhores na otimização de uma variável em um registro, do que fazendo o mesmo com um resultado intermediário de um cálculo. Seqüências longas de tarefas usando um único operador que levam ao resultado final podem ser otimizadas melhor do que uma equação longa e complicada. Otimizadores mais recentes podem ter reduzido a diferença entre código limpo e complicado, mas duvido que tenham eliminado.
Outras otimizações, como desenrolamento de loop, podem ser adicionadas de maneira limpa, quando necessário.
Qualquer otimização adicionada para melhorar o desempenho deve ser acompanhada de um comentário apropriado. Isso deve incluir uma declaração de que foi adicionado como uma otimização, de preferência com medidas de desempenho antes e depois.
Eu descobri que a regra 80/20 se aplica ao código que otimizei. Como regra geral, não otimizo nada que não esteja demorando pelo menos 80% do tempo. Em seguida, viso (e geralmente alcanço) um aumento de 10 vezes no desempenho. Isso melhora o desempenho em cerca de 4 vezes. A maioria das otimizações implementadas não tornou o código significativamente menos "bonito". Sua milhagem pode variar.
fonte
Se feio, você quer dizer difícil de ler / entender no nível em que outros desenvolvedores o reutilizarão ou precisarão entendê-lo, então eu diria que código elegante e fácil de ler quase sempre resultará em uma rede. ganho de desempenho a longo prazo em um aplicativo que você deve manter.
Caso contrário, às vezes há uma vitória de desempenho suficiente para fazer valer a pena colocar uma coisa feia em uma caixa bonita com uma interface matadora, mas, na minha experiência, esse é um dilema bastante raro.
Pense em evitar o trabalho básico à medida que avança. Guarde os truques misteriosos para quando um problema de desempenho realmente se apresentar. E se você precisar escrever algo que alguém possa entender apenas através da familiarização com a otimização específica, faça o que puder para, pelo menos, tornar o feio fácil de entender a partir da reutilização do seu ponto de vista do código. Código que executa miseravelmente raramente o faz porque os desenvolvedores estavam pensando demais sobre o que o próximo cara herdaria, mas se alterações frequentes são a única constante de um aplicativo (a maioria dos aplicativos da web na minha experiência), código rígido / inflexível difícil de modificar está praticamente implorando por bagunças em pânico para começar a aparecer em toda a sua base de código. Limpo e enxuto é melhor para o desempenho a longo prazo.
fonte
Complexo e feio não são a mesma coisa. Código que possui muitos casos especiais, otimizado para absorver toda a última gota de desempenho, e que se assemelha a um emaranhado de conexões e dependências, pode de fato ser muito cuidadosamente projetado e bonito quando você o entender. De fato, se o desempenho (medido em termos de latência ou outra coisa) é importante o suficiente para justificar código muito complexo, o código deve ser bem projetado. Caso contrário, não poderá ter certeza de que toda essa complexidade é realmente melhor do que uma solução mais simples.
Código feio, para mim, é um código desleixado, mal considerado e / ou desnecessariamente complicado. Eu não acho que você desejaria algum desses recursos no código que precisam ser executados.
fonte
Eu trabalho em um campo que é um pouco mais focado na taxa de transferência do que na latência, mas é muito crítico para o desempenho e eu diria "meio que" .
No entanto, um problema é que muitas pessoas entendem completamente suas noções de desempenho. Os iniciantes geralmente entendem tudo errado e todo o seu modelo conceitual de "custo computacional" precisa ser reformulado, com apenas a complexidade algorítmica sendo a única coisa que eles podem acertar. Intermediários entendem muitas coisas erradas. Especialistas entendem algumas coisas erradas.
Medir com ferramentas precisas que podem fornecer métricas como falhas de cache e previsões incorretas de ramificações é o que mantém todas as pessoas com qualquer nível de conhecimento em campo sob controle.
Medir é também o que aponta o que não otimizar . Os especialistas costumam gastar menos tempo otimizando do que os novatos, já que estão otimizando pontos de acesso medidos verdadeiros e não tentando otimizar facadas selvagens no escuro com base em palpites sobre o que poderia ser lento (o que, de forma extrema, poderia tentar micro-otimizar apenas sobre todas as outras linhas da base de código).
Projetando para o desempenho
Com isso de lado, a chave para o design de desempenho vem da parte do design , como no design de interface. Um dos problemas com a inexperiência é que tende a haver uma mudança precoce nas métricas absolutas de implementação, como o custo de uma chamada de função indireta em algum contexto generalizado, como se o custo (que é melhor compreendido em sentido imediato do ponto de vista de um otimizador) ponto de vista de ramificação) é um motivo para evitá-lo por toda a base de código.
Os custos são relativos . Embora exista um custo para uma chamada de função indireta, por exemplo, todos os custos são relativos. Se você está pagando esse custo uma vez para chamar uma função que percorre milhões de elementos, se preocupar com esse custo é como passar horas pechinchando moedas de um centavo para a compra de um produto de um bilhão de dólares, apenas para concluir que não o comprará porque era um centavo muito caro.
Design de interface mais grosseiro
O aspecto do design de interface do desempenho geralmente busca elevar esses custos a um nível mais grosso. Em vez de pagar os custos de abstração de tempo de execução para uma única partícula, por exemplo, podemos elevar esse custo ao nível do sistema / emissor de partículas, tornando efetivamente uma partícula em um detalhe de implementação e / ou simplesmente dados brutos dessa coleção de partículas.
Portanto, o design orientado a objetos não precisa ser incompatível com o design para desempenho (latência ou taxa de transferência), mas pode haver tentações em uma linguagem focada nele para modelar objetos granulares cada vez menores, e o otimizador mais recente não pode Socorro. Ele não pode fazer coisas como unir uma classe que representa um único ponto de uma maneira que produz uma representação SoA eficiente para os padrões de acesso à memória do software. Uma coleção de pontos com o design de interface modelado no nível de grosseria oferece essa oportunidade e permite a iteração em direção a soluções cada vez mais otimizadas, conforme necessário. Esse design foi desenvolvido para memória em massa *.
Muitos projetos críticos para o desempenho podem realmente ser muito compatíveis com a noção de design de interface de alto nível que é fácil para os seres humanos entenderem e usarem. A diferença é que "alto nível" nesse contexto seria sobre agregação em massa de memória, uma interface modelada para coleções de dados potencialmente grandes e com uma implementação oculta que pode ser de nível bastante baixo. Uma analogia visual pode ser um carro realmente confortável, fácil de dirigir, manusear e muito seguro, na velocidade do som, mas se você abrir o capô, haverá pequenos demônios que cospem fogo no interior.
Com um design mais grosso, também tende a ser uma maneira mais fácil de fornecer padrões de bloqueio mais eficientes e explorar o paralelismo no código (multithreading é um assunto exaustivo que eu vou pular aqui).
Conjunto de memórias
Um aspecto crítico da programação de baixa latência provavelmente será um controle muito explícito sobre a memória para melhorar a localidade de referência, bem como apenas a velocidade geral de alocar e desalocar memória. Na verdade, uma memória de pool de alocador personalizado ecoa o mesmo tipo de mentalidade de design que descrevemos. Ele foi projetado para granel ; foi projetado em um nível aproximado. Ele pré-aloca a memória em grandes blocos e agrupa a memória já alocada em pequenos blocos.
A idéia é exatamente a mesma de empurrar coisas caras (alocar um pedaço de memória contra um alocador de uso geral, por exemplo) para um nível cada vez mais grosso. Um conjunto de memórias foi projetado para lidar com memória em massa .
Sistemas de tipo segregam memória
Uma das dificuldades do design granular orientado a objetos em qualquer idioma é que ele geralmente quer introduzir muitos tipos e estruturas de dados definidas pelo usuário. Esses tipos podem querer ser alocados em pequenos pedaços pequenos, se forem alocados dinamicamente.
Um exemplo comum em C ++ seria nos casos em que o polimorfismo é necessário, onde a tentação natural é alocar cada instância de uma subclasse contra um alocador de memória de uso geral.
Isso acaba desmembrando layouts de memória possivelmente contíguos em pequenos bits e pedaços espalhados pelo intervalo de endereços, o que se traduz em mais falhas de página e falhas de cache.
Os campos que exigem a resposta determinística de menor latência, sem gagueira são provavelmente o único lugar onde os pontos de acesso nem sempre se resumem a um único gargalo, onde pequenas ineficiências podem realmente realmente se "acumular" (algo que muitas pessoas imaginam acontecendo incorretamente com um criador de perfil para mantê-los sob controle, mas em campos controlados por latência, pode haver alguns casos raros em que pequenas ineficiências se acumulam). E muitas das razões mais comuns para esse acúmulo podem ser as seguintes: a alocação excessiva de pequenos pedaços de memória em todo o lugar.
Em linguagens como Java, pode ser útil usar mais matrizes de tipos de dados antigos simples quando possível para áreas de gargalo (áreas processadas em loops apertados), como uma matriz de
int
(mas ainda por trás de uma interface de alto nível volumosa) em vez de, digamos , umArrayList
dosInteger
objetos definidos pelo usuário . Isso evita a segregação de memória que normalmente acompanha o último. No C ++, não precisamos degradar a estrutura tanto se nossos padrões de alocação de memória forem eficientes, pois os tipos definidos pelo usuário podem ser alocados de forma contígua e mesmo no contexto de um contêiner genérico.Fundindo a memória novamente
Uma solução aqui é buscar um alocador personalizado para tipos de dados homogêneos e, possivelmente, mesmo entre tipos de dados homogêneos. Quando pequenos tipos de dados e estruturas de dados são achatadas em bits e bytes na memória, elas assumem uma natureza homogênea (embora com alguns requisitos variados de alinhamento). Quando não os olhamos de uma mentalidade centrada na memória, o sistema de tipos de linguagens de programação "deseja" dividir / segregar regiões de memória potencialmente contíguas em pequenos pedaços dispersos.
A pilha utiliza esse foco centralizado na memória para evitar isso e potencialmente armazena qualquer combinação mista possível de instâncias do tipo definido pelo usuário. Utilizar mais a pilha é uma ótima idéia, quando possível, pois quase sempre ela fica em uma linha de cache, mas também podemos projetar alocadores de memória que imitam algumas dessas características sem um padrão LIFO, fundindo a memória entre tipos de dados diferentes em tipos de dados contíguos. até mesmo para padrões mais complexos de alocação e desalocação de memória.
O hardware moderno foi projetado para atingir seu pico ao processar blocos contíguos de memória (acessando repetidamente a mesma linha de cache, a mesma página, por exemplo). A palavra-chave existe contiguidade, pois isso só é benéfico se houver dados de interesse ao redor. Portanto, grande parte da chave (mas também dificuldade) do desempenho é reunir novamente pedaços de memória segregados em blocos contíguos que são acessados na íntegra (todos os dados ao redor são relevantes) antes da remoção. O sistema de tipos avançados de tipos especialmente definidos pelo usuário em linguagens de programação pode ser o maior obstáculo aqui, mas sempre podemos procurar e resolver o problema por meio de um alocador personalizado e / ou projetos mais volumosos, quando apropriado.
Feio
"Feio" é difícil de dizer. É uma métrica subjetiva, e alguém que trabalha em um campo muito crítico de desempenho começará a mudar sua idéia de "beleza" para uma que é muito mais orientada a dados e se concentra nas interfaces que processam as coisas em massa.
Perigoso
"Perigoso" pode ser mais fácil. Em geral, o desempenho tende a alcançar um código de nível inferior. A implementação de um alocador de memória, por exemplo, é impossível sem chegar abaixo dos tipos de dados e trabalhar no nível perigoso de bits e bytes brutos. Como resultado, pode ajudar a aumentar o foco em procedimentos de teste cuidadosos nesses subsistemas críticos para o desempenho, dimensionando a profundidade dos testes com o nível de otimizações aplicado.
Beleza
No entanto, tudo isso estaria no nível de detalhes da implementação. Tanto em uma mentalidade veterana em larga escala quanto em crítica ao desempenho, a "beleza" tende a mudar para designs de interface, em vez de detalhes de implementação. Torna-se uma prioridade exponencialmente mais alta buscar interfaces "bonitas", utilizáveis, seguras e eficientes, em vez de implementações devido a falhas de acoplamento e cascata que podem ocorrer diante de uma alteração no design da interface. As implementações podem ser trocadas a qualquer momento. Normalmente, iteramos para o desempenho conforme necessário e conforme indicado pelas medições. A chave do design da interface é modelar em um nível grosso o suficiente para deixar espaço para essas iterações sem interromper o sistema inteiro.
De fato, eu sugeriria que o foco de um veterano no desenvolvimento crítico de desempenho tende a colocar predominantemente um foco predominante em segurança, testes, manutenibilidade, apenas o discípulo da SE em geral, uma vez que uma base de código em larga escala que possui vários desempenhos Os subsistemas críticos (sistemas de partículas, algoritmos de processamento de imagem, processamento de vídeo, feedback de áudio, rastreadores de raios, mecanismos de malha etc.) precisarão prestar muita atenção à engenharia de software para evitar afogamentos em um pesadelo de manutenção. Não é por mera coincidência que muitas vezes os produtos mais surpreendentemente eficientes do mercado também podem ter o menor número de bugs.
TL; DR
De qualquer forma, essa é minha opinião sobre o assunto, variando de prioridades em campos genuinamente críticos para o desempenho, o que pode reduzir a latência e fazer com que pequenas ineficiências se acumulem, e o que realmente constitui "beleza" (quando se olha para as coisas de maneira mais produtiva).
fonte
Não deve ser diferente, mas aqui está o que eu faço:
Escreva de forma limpa e sustentável.
Faça o diagnóstico de desempenho e corrija os problemas que ele indicar, não os que você adivinha. Garantido, eles serão diferentes do que você espera.
Você pode fazer essas correções de uma maneira que ainda seja clara e sustentável, mas precisará adicionar comentários para que as pessoas que olham o código saibam por que você fez dessa maneira. Caso contrário, eles desfarão.
Então, existe uma troca? Eu realmente não penso assim.
fonte
Você pode escrever um código feio que é muito rápido e também um código bonito que é tão rápido quanto o seu código feio. O gargalo não estará na beleza / organização / estrutura do seu código, mas nas técnicas que você escolheu. Por exemplo, você está usando soquetes sem bloqueio? Você está usando design de thread único? Você está usando uma fila sem bloqueio para comunicação entre threads? Você está produzindo lixo para o GC? Você está executando alguma operação de E / S de bloqueio no encadeamento crítico? Como você pode ver, isso não tem nada a ver com beleza.
fonte
O que importa para o usuário final?
Caso 1: código incorreto otimizado
Caso 2: código bom não otimizado
Solução?
Fácil, otimize trechos de código críticos de desempenho
por exemplo:
Um programa que consiste em 5 métodos , 3 deles são para gerenciamento de dados, 1 para leitura de disco e outro para gravação de disco
Esses 3 métodos de gerenciamento de dados usam os dois métodos de E / S e dependem deles
Otimizamos os métodos de E / S.
Motivo: É menos provável que os métodos de E / S sejam alterados, nem afetam o design do aplicativo. No geral, tudo nesse programa depende deles e, portanto, eles parecem críticos para o desempenho, usaríamos qualquer código para otimizá-los. .
Isso significa que obtemos um bom código e design gerenciável do programa, mantendo-o rápido, otimizando certas partes do código
Eu estou pensando..
Eu acho que código ruim dificulta a otimização do polonês e pequenos erros podem torná-lo ainda pior, então um bom código para iniciantes / iniciantes seria melhor se fosse bem escrito esse código feio.
fonte