Há um arquivo de origem em um projeto bastante grande, com várias funções extremamente sensíveis ao desempenho (chamadas milhões de vezes por segundo). De fato, o mantenedor anterior decidiu escrever 12 cópias de uma função, cada uma com uma diferença muito pequena, para economizar o tempo que seria gasto verificando as condicionais em uma única função.
Infelizmente, isso significa que o código é uma PITA para manter. Gostaria de remover todo o código duplicado e escrever apenas um modelo. No entanto, a linguagem Java não oferece suporte a modelos e não tenho certeza de que os genéricos sejam adequados para isso.
Meu plano atual é escrever um arquivo que gere as 12 cópias da função (um expansor de modelo de uso único, praticamente). É claro que eu forneceria uma explicação abundante sobre por que o arquivo deve ser gerado programaticamente.
Minha preocupação é que isso levaria à confusão de futuros mantenedores e talvez introduza erros desagradáveis se eles esquecerem de regenerar o arquivo após modificá-lo ou (ainda pior) se eles modificarem o arquivo gerado por programação. Infelizmente, além de reescrever tudo no C ++, não vejo como corrigir isso.
Os benefícios dessa abordagem superam as desvantagens? Em vez disso, devo:
- Aceite o desempenho e use uma função única e sustentável.
- Adicione explicações sobre por que a função deve ser duplicada 12 vezes e aceite a carga de manutenção.
- Tente usar os genéricos como modelos (eles provavelmente não funcionam dessa maneira).
- Grite com o antigo mantenedor por tornar o código tão dependente do desempenho em uma única função.
- Outro método para manter o desempenho e a capacidade de manutenção?
PS Devido ao mau design do projeto, a criação de perfil da função é bastante complicada ... no entanto, o ex-mantenedor me convenceu de que o desempenho atingido é inaceitável. Suponho que ele queira dizer mais de 5%, embora isso seja um palpite completo da minha parte.
Talvez eu deva elaborar um pouco. As 12 cópias fazem uma tarefa muito semelhante, mas têm pequenas diferenças. As diferenças estão em vários lugares em toda a função, então, infelizmente, existem muitas, muitas, instruções condicionais. Existem efetivamente 6 "modos" de operação e 2 "paradigmas" de operação (palavras compostas por mim). Para usar a função, especifica-se o "modo" e o "paradigma" da operação. Isso nunca é dinâmico; cada pedaço de código usa exatamente um modo e paradigma. Todos os 12 pares de paradigmas de modo são usados em algum lugar do aplicativo. As funções são nomeadas apropriadamente func1 a func12, com números pares representando o segundo paradigma e números ímpares representando o primeiro paradigma.
Estou ciente de que esse é o pior design de todos os tempos, se a manutenção for o objetivo. Mas parece ser "rápido o suficiente" e esse código não precisa de alterações há um tempo ... Também vale a pena notar que a função original não foi excluída (embora seja um código morto, tanto quanto eu posso dizer) , portanto, a refatoração seria simples.
fonte
Makefile
" (ou de qualquer sistema que você use) e remova-o logo após a compilação terminar . Dessa forma, eles simplesmente não têm a chance de modificar o arquivo de origem errado.Respostas:
Esta é uma situação muito ruim, você precisa refatorar este o mais rápido possível - este é dívida técnica em que é pior - você nem sabe como é importante o código realmente é - única especulam que é importante.
Quanto às soluções o mais rápido possível:
Algo que pode ser feito é adicionar uma etapa de compilação personalizada. Se você usar o Maven, na verdade, é bastante simples de fazer, outros sistemas de compilação automatizados provavelmente também lidarão com isso. Escreva um arquivo com uma extensão diferente de .java e adicione uma etapa personalizada que procure na sua fonte arquivos como esse e gere novamente o arquivo .java real. Você também pode adicionar um aviso de isenção de responsabilidade enorme ao arquivo gerado automaticamente, explicando para não modificá-lo.
Prós versus uso de um arquivo gerado uma vez: seus desenvolvedores não obterão as alterações no .java funcionando. Se eles realmente executarem o código em sua máquina antes de confirmar, descobrirão que suas alterações não terão efeito (hah). E então talvez eles leiam o aviso. Você está absolutamente certo em não confiar em seus colegas de equipe e no seu futuro, lembrando que esse arquivo em particular precisa ser alterado de uma maneira diferente. Também permite testes automáticos, pois o JUnit compila seu programa antes de executar os testes (e regenera o arquivo também)
EDITAR
A julgar pelos comentários, a resposta surgiu como se essa fosse uma maneira de fazer com que isso funcionasse indefinidamente e que talvez fosse bom implantar em outras partes críticas de desempenho do seu projeto.
Simplificando: não é.
O fardo extra de criar sua própria mini-linguagem, escrever um gerador de código e mantê-lo, sem mencionar ensiná-lo a futuros mantenedores é infernal a longo prazo. O acima exposto permite apenas uma maneira mais segura de lidar com o problema enquanto você estiver trabalhando em uma solução de longo prazo . O que isso levará está acima de mim.
fonte
ant clean
qualquer equivalente. Não é necessário colocar um aviso em um arquivo que nem está lá!A manutenção é real ou apenas o incomoda? Se isso apenas o incomoda, deixe-o em paz.
O problema de desempenho é real ou o desenvolvedor anterior apenas pensou que era? Os problemas de desempenho, na maioria das vezes, não estão onde deveriam estar, mesmo quando um criador de perfil é usado. (Eu uso essa técnica para encontrá-los com segurança.)
Portanto, existe a possibilidade de você ter uma solução feia para um não-problema, mas também pode ser uma solução feia para um problema real. Em caso de dúvida, deixe em paz.
fonte
Realmente, certifique-se de que, sob condições reais de produção (consumo de memória realista, que aciona a coleta de lixo de forma realista, etc.), todos esses métodos separados realmente fazem diferença no desempenho (em vez de ter apenas um método). Isso levará alguns dias para o seu tempo, mas você poderá economizar semanas, simplificando o código com o qual trabalha a partir de agora.
Além disso, se você faz descobrir que você precisa de todas as funções, você pode querer usar Javassist para gerar o código de programação. Como outros já apontaram, você pode automatizá-lo com o Maven .
fonte
Por que não incluir algum tipo de pré-processador de modelo \ geração de código para este sistema? Uma etapa de criação extra personalizada que executa e emite arquivos de origem java extras antes de compilar o restante do código. É assim que a inclusão de clientes Web fora dos arquivos wsdl e xsd geralmente funciona.
Obviamente, você terá a manutenção desse pré-processador \ gerador de código, mas não precisará se preocupar com a manutenção duplicada do código principal.
Como o código Java em tempo de compilação está sendo emitido, não há multa de desempenho a ser paga pelo código extra. Mas você ganha simplificação de manutenção ao ter o modelo em vez de todo o código duplicado.
Os genéricos Java não oferecem benefício de desempenho devido ao apagamento do tipo na linguagem, a conversão simples é usada em seu lugar.
Do meu entendimento dos modelos C ++, eles são compilados em várias funções por cada invocação de modelo. Você terminará com um código de tempo de compilação intermediário duplicado para cada tipo armazenado em um vetor std :: vector, por exemplo.
fonte
Nenhum método Java sadio pode ser longo o suficiente para ter 12 variantes ... e o JITC odeia métodos longos - simplesmente se recusa a otimizá-los adequadamente. Vi um fator de aceleração de dois simplesmente dividindo um método em dois mais curtos. Talvez este seja o caminho a seguir.
OTOH com várias cópias pode fazer sentido mesmo se forem idênticas. À medida que cada um deles é usado em locais diferentes, eles são otimizados para casos diferentes (o JITC os perfila e coloca os casos raros em um caminho excepcional.
Eu diria que gerar código não é grande coisa, supondo que haja uma boa razão. A sobrecarga é bastante baixa e os arquivos nomeados corretamente levam imediatamente à sua origem. Há muito tempo atrás, quando eu gerava código fonte, eu colocava
// DO NOT EDIT
todas as linhas ... Eu acho que isso economiza o suficiente.fonte
Não há absolutamente nada de errado com o 'problema' que você mencionou. Pelo que sei, esse é o tipo exato de design e abordagem usado pelo servidor DB para ter um bom desempenho.
Eles têm muitos métodos especiais para garantir que possam maximizar o desempenho para todos os tipos de operações: ingressar, selecionar, agregar, etc ... quando determinadas condições se aplicarem.
Em resumo, a geração de código como a que você acha que é uma má ideia. Talvez você deva examinar este diagrama para ver como um banco de dados resolve um problema semelhante ao seu:
fonte
Você pode verificar se ainda pode usar algumas abstrações sensíveis e, por exemplo, o padrão do método do modelo para escrever código compreensível para a funcionalidade comum e mover as diferenças entre os métodos para as "operações primitivas" (de acordo com a descrição do padrão) em 12 subclasses. Isso pode melhorar a capacidade de manutenção e a capacidade de teste e, na verdade, pode ter o mesmo desempenho que o código atual, uma vez que a JVM pode incorporar o método às chamadas primitivas após um tempo. Obviamente, você deve verificar isso em um teste de desempenho.
fonte
Você pode resolver o problema de métodos especializados usando a linguagem Scala.
O Scala pode incorporar métodos, isso (em combinação com o uso fácil de funções de ordem superior) torna possível evitar a duplicação de código gratuitamente - esse parece o principal problema mencionado em sua resposta.
Mas também, o Scala possui macros sintáticas, o que possibilita fazer muitas coisas com código no tempo de compilação de maneira segura.
E o problema comum dos tipos primitivos de boxe quando usados em genéricos também é possível resolver no Scala: ele pode fazer especialização genérica para primitivos para evitar o boxe automaticamente usando
@specialized
anotações - isso é incorporado à própria linguagem. Então, basicamente, você escreverá um método genérico no Scala, e ele será executado com a velocidade de métodos especializados. E se você também precisa de aritmética genérica rápida, é fácil fazê-lo usando o "padrão" Typeclass para injetar as operações e valores para diferentes tipos numéricos.Além disso, Scala é incrível de muitas outras maneiras. E você não precisa reescrever todo o código no Scala, porque a interoperabilidade do Scala <-> Java é excelente. Apenas certifique-se de usar o SBT (ferramenta de compilação scala) para criar o projeto.
fonte
Se a função em questão for grande, transformar os bits de "modo / paradigma" em uma interface e transmitir um objeto que implementa essa interface como parâmetro para a função pode funcionar. O GoF chama isso de padrão de "Estratégia", iirc. Se a função for pequena, a sobrecarga aumentada pode ser significativa ... alguém já mencionou a criação de perfil? ... mais pessoas devem mencionar a criação de perfil.
fonte
Quantos anos tem o programa? Provavelmente, o hardware mais recente removeria o gargalo e você poderia mudar para uma versão mais sustentável. Mas é preciso haver um motivo para a manutenção; portanto, a menos que sua tarefa seja melhorar a base de código, deixe-a como está funcionando.
fonte