O modelo “metaprogramação” em Java é uma boa idéia?

29

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.

Fengyang Wang
fonte
Se o resultado do desempenho for grave o suficiente para justificar 12 versões da função, você poderá ficar preso a ela. Se você refatorar para uma única função (ou usar genéricos), o desempenho atingido seria ruim o suficiente para perder clientes e negócios?
FrustratedWithFormsDesigner
12
Então eu acho que você precisa fazer seus próprios testes (eu sei que você disse que a criação de perfil é complicada, mas você ainda pode fazer testes de desempenho "do tipo caixa-preta" do sistema como um todo, certo?) Para ver a diferença existente é. E se for perceptível, acho que você pode estar preso ao gerador de código.
FrustratedWithFormsDesigner
1
Isso pode parecer que poderia ter se beneficiado de algum polimorfismo paramétrico (ou talvez até genérico), em vez de chamar cada função por um nome diferente.
Robert Harvey
2
Nomear as funções func1 ... func12 parece insano. Pelo menos nomeie-os mode1Par2 etc ... ou talvez myFuncM3P2.
user949300
2
"se eles modificarem o arquivo gerado por programação" ... crie o arquivo somente ao criar a partir do " 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.
Bakuriu

Respostas:

23

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.

Ordous
fonte
19
Desculpe, mas tenho que discordar. Você não sabe o suficiente sobre o código do OP para tomar essa decisão por ele. A geração de código parece-me que contém mais dívida técnica do que a solução original.
Robert Harvey
6
O comentário de Robert não pode ser exagerado. Sempre que você tiver um "aviso de isenção de responsabilidade para não modificar um arquivo", isso é o equivalente a "dívida técnica" de uma loja de cheques administrada por um agente de apostas ilegal apelidado de "Tubarão".
corsiKa
8
Obviamente, o arquivo gerado automaticamente não pertence ao repositório de origem (não é, afinal, é um código compilado) e deve ser removido por ant cleanqualquer equivalente. Não é necessário colocar um aviso em um arquivo que nem está lá!
Jörg W Mittag
2
@RobertHarvey Não estou tomando nenhuma decisão para o OP. O OP perguntou se é uma boa idéia ter esses modelos da maneira que ele os possui e propus uma maneira (melhor) de mantê-los. Não é uma solução ideal e, de fato, como afirmei na primeira frase, toda a situação é ruim e uma maneira muito melhor de avançar é remover o problema, não fazer uma solução instável. Isso inclui avaliar adequadamente quão crítico é esse código, quão ruim é o desempenho atingido e se existem maneiras de fazê-lo funcionar sem escrever muito código incorreto.
Ordous
2
@corsiKa Eu nunca disse que essa é uma solução duradoura. Isso seria tanto uma dívida quanto a situação original. A única coisa que faz é diminuir a volatilidade até que uma solução sistemática seja encontrada. O que provavelmente incluirá a mudança totalmente para uma nova plataforma / estrutura / linguagem, pois se você estiver enfrentando problemas como este em Java - estará fazendo algo extremamente complexo ou extremamente errado.
Ordous
16

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.

Mike Dunlavey
fonte
10

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 .

Shivan Dragon
fonte
2
Além disso, mesmo que os problemas de desempenho sejam reais, o exercício de estudá-los ajudará você a conhecer melhor o que é possível com o seu código.
Carl Manaster
6

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.

Peter Smith
fonte
2

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 EDITtodas as linhas ... Eu acho que isso economiza o suficiente.

maaartinus
fonte
1

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:insira a descrição da imagem aqui

randomA
fonte
1

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.

Hans-Peter Störr
fonte
0

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 @specializedanotaçõ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.

Sarge Borsch
fonte
-1

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.

mjfgates
fonte
isso parece mais um comentário do que uma resposta
gnat
-2

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.

user138623
fonte
1
isso parece meramente repetir os pontos feitos e explicados em uma resposta anterior postada algumas horas atrás
gnat
1
A única maneira de determinar onde um gargalo é realmente é traçar um perfil - como mencionado na outra resposta. Sem essas informações importantes, qualquer correção sugerida para o desempenho é pura especulação.