Il modello "metaprogrammazione" in Java è una buona idea?


29

Esiste un file sorgente in un progetto piuttosto grande con diverse funzioni che sono estremamente sensibili alle prestazioni (chiamate milioni di volte al secondo). In effetti, il precedente manutentore aveva deciso di scrivere 12 copie di una funzione ognuna leggermente diversa, al fine di risparmiare il tempo che sarebbe trascorso a controllare i condizionali in un'unica funzione.

Sfortunatamente, questo significa che il codice è un PITA da mantenere. Vorrei rimuovere tutto il codice duplicato e scrivere solo un modello. Tuttavia, il linguaggio, Java, non supporta i modelli e non sono sicuro che i generici siano adatti a questo.

Il mio piano attuale è quello di scrivere invece un file che genera le 12 copie della funzione (praticamente un expander modello monouso). Ovviamente fornirei una spiegazione abbondante del perché il file deve essere generato a livello di codice.

La mia preoccupazione è che ciò porterebbe alla confusione dei futuri manutentori, e forse introdurrebbe cattivi bug se dimenticano di rigenerare il file dopo averlo modificato, o (anche peggio) se modificano invece il file generato a livello di programmazione. Sfortunatamente, a parte riscrivere il tutto in C ++, non vedo alcun modo per risolvere questo problema.

I vantaggi di questo approccio superano gli svantaggi? Dovrei invece:

  • Assumi il colpo di performance e usa un'unica funzione mantenibile.
  • Aggiungi spiegazioni sul motivo per cui la funzione deve essere duplicata 12 volte e sopporta con cura gli oneri di manutenzione.
  • Tentativo di utilizzare generici come modelli (probabilmente non funzionano in questo modo).
  • Urla al vecchio manutentore per aver reso il codice così dipendente dalle prestazioni su una singola funzione.
  • Altro metodo per mantenere prestazioni e manutenibilità?

PS A causa della cattiva progettazione del progetto, la profilazione della funzione è piuttosto complicata ... tuttavia, l'ex manutentore mi ha convinto che l'hit di performance è inaccettabile. Suppongo che questo significhi più del 5%, anche se è una mia ipotesi completa da parte mia.


Forse dovrei elaborare un po '. Le 12 copie svolgono un compito molto simile, ma presentano differenze minime. Le differenze sono diverse in tutta la funzione, quindi sfortunatamente ci sono molte, molte dichiarazioni condizionali. Esistono effettivamente 6 "modalità" di operazione e 2 "paradigmi" di operazione (parole inventate da me stesso). Per utilizzare la funzione, si specifica la "modalità" e il "paradigma" dell'operazione. Questo non è mai dinamico; ogni parte di codice utilizza esattamente una modalità e un paradigma. Tutte e 12 le coppie modalità-paradigma sono utilizzate da qualche parte nell'applicazione. Le funzioni sono appropriatamente chiamate func1 a func12, con numeri pari che rappresentano il secondo paradigma e numeri dispari che rappresentano il primo paradigma.

Sono consapevole che questo è il peggior progetto di sempre se l'obiettivo è la manutenibilità. Ma sembra essere "abbastanza veloce", e questo codice non ha avuto bisogno di modifiche da un po 'di tempo ... Vale anche la pena notare che la funzione originale non è stata eliminata (anche se è un codice morto per quanto posso dire) , quindi il refactoring sarebbe semplice.


Se il colpo di performance è abbastanza grave da giustificare 12 versioni della funzione, allora potresti esserne bloccato. Se si esegue il refactoring su una singola funzione (o si utilizzano generici), il risultato potrebbe essere abbastanza grave da perdere clienti e affari?
FrustratedWithFormsDesigner,

12
Quindi penso che tu debba fare i tuoi test (so che hai detto che la profilazione è complicata, ma puoi ancora fare test di performance "black box" del sistema nel suo insieme, giusto?) Per vedere quanta differenza c'è è. E se è evidente, allora penso che potresti essere bloccato con il generatore di codice.
FrustratedWithFormsDesigner,

1
Questo potrebbe sembrare che avrebbe potuto beneficiare di alcuni polimorfismi parametrici (o forse anche dei generici), piuttosto che chiamare ogni funzione con un nome diverso.
Robert Harvey,

2
La denominazione delle funzioni func1 ... func12 sembra folle. Almeno chiamali mode1Par2 ecc ... o forse myFuncM3P2.
user949300,

2
"se modificano invece il file generato a livello di codice" ... crea il file solo quando viene compilato dal " Makefile" (o qualunque sistema tu usi) e rimuovilo dopo aver completato la compilazione . In questo modo semplicemente non hanno la possibilità di modificare il file sorgente errato.
Bakuriu,

Risposte:


23

Questa è una situazione molto brutta, devi rifattorizzare al più presto - questo è il debito tecnico nel peggiore dei casi - non sai nemmeno quanto sia veramente importante il codice - solo speculare che sia importante.

Per quanto riguarda le soluzioni al più presto:
Qualcosa che può essere fatto è l'aggiunta di un passaggio di compilazione personalizzato. Se usi Maven che è in realtà abbastanza semplice da fare, è probabile che anche altri sistemi di compilazione automatizzati faranno fronte a questo. Scrivi un file con un'estensione diversa da .java e aggiungi un passaggio personalizzato che cerca i file in questo modo nella tua fonte e rigenera il vero .java. Potresti anche voler aggiungere un enorme disclaimer sul file generato automaticamente che spiega di non modificarlo.

Pro vs usando un file generato una volta: i tuoi sviluppatori non faranno funzionare le loro modifiche al .java. Se eseguono effettivamente il codice sulla propria macchina prima di impegnarsi, scopriranno che le loro modifiche non hanno alcun effetto (hah). E poi forse leggeranno il disclaimer. Hai assolutamente ragione a non fidarti dei tuoi compagni di squadra e del tuo sé futuro ricordando che questo particolare file deve essere cambiato in un modo diverso. Consente anche test automatici, poiché JUnit compilerà il programma prima di eseguire i test (e rigenererà anche il file)

MODIFICARE

A giudicare dai commenti, la risposta è venuta fuori come se questo fosse un modo per farlo funzionare indefinitamente e forse OK per essere distribuito ad altre parti critiche delle prestazioni del progetto.

In poche parole: non lo è.

L'onere aggiuntivo di creare il proprio mini-linguaggio, scrivere un generatore di codice per esso e mantenerlo, per non parlare dell'insegnamento ai futuri manutentori è a lungo termine infernale. Quanto sopra consente solo un modo più sicuro di gestire il problema mentre si lavora su una soluzione a lungo termine . Quello che ci vorrà è sopra di me.


19
Scusa, ma non sono d'accordo. Non sai abbastanza del codice del PO per prendere questa decisione per lui. La generazione del codice mi sembra che contenga più debito tecnico rispetto alla soluzione originale.
Robert Harvey,

6
Il commento di Robert non può essere sopravvalutato. Ogni volta che hai un "disclaimer che spiega di non modificare un file" che è l'equivalente del "debito tecnico" di un negozio di assegni gestito da un allibratore illegale soprannominato "Shark".
corsiKa

8
Il file generato automaticamente ovviamente non appartiene al repository di origine (non è fonte, dopotutto, è codice compilato) e dovrebbe essere rimosso da ant cleano qualunque sia il tuo equivalente. Non c'è bisogno di mettere un disclaimer in un file che non è nemmeno lì!
Jörg W Mittag,

2
@RobertHarvey Non prendo alcuna decisione per l'OP. OP ha chiesto se è una buona idea avere tali modelli nel modo in cui li ha e ho proposto un modo (migliore) per mantenerli. Non è una soluzione ideale e, in effetti, come ho affermato nella prima frase, l'intera situazione è negativa e un modo molto migliore per andare avanti è rimuovere il problema, non farne una soluzione traballante. Ciò include una corretta valutazione di quanto sia critico questo codice, quanto è grave il danno alle prestazioni e se ci sono modi per farlo funzionare senza scrivere molto codice cattivo.
Ordous,

2
@corsiKa Non ho mai detto che questa sia una soluzione duratura. Questo sarebbe tanto un debito quanto la situazione originale. L'unica cosa che fa è ridurre la volatilità fino a trovare una soluzione sistematica. Il che probabilmente includerà il passaggio interamente a una nuova piattaforma / framework / linguaggio poiché se si verificano problemi come questo in Java - si sta facendo qualcosa di estremamente complesso o estremamente sbagliato.
Ordous,

16

Il colpo di manutenzione è reale o ti dà fastidio? Se ti disturba, lascialo in pace.

Il problema delle prestazioni è reale o lo sviluppatore precedente pensava solo che lo fosse? I problemi di prestazioni, il più delle volte, non sono dove si pensa che siano, anche quando viene utilizzato un profiler. (Uso questa tecnica per trovarli in modo affidabile.)

Quindi c'è la possibilità che tu abbia una brutta soluzione a un non-problema, ma potrebbe anche essere una brutta soluzione a un vero problema. In caso di dubbio, lasciarlo da solo.


10

Assicurati davvero che in condizioni reali, di produzione, (consumo di memoria realistico, che inneschi Garbage Collection in modo realistico, ecc.), Tutti questi metodi separati fanno davvero la differenza in termini di prestazioni (invece di avere un solo metodo). Questo richiederà alcuni giorni di tempo, ma potrebbe farti risparmiare settimane semplificando il codice con cui lavori d'ora in poi.

Inoltre, se si fa scoprire che avete bisogno di tutte le funzioni, si potrebbe desiderare di utilizzare Javassist per generare il codice programatically. Come altri hanno sottolineato, puoi automatizzarlo con Maven .


2
Inoltre, anche se risulta che i problemi di prestazione sono reali, l'esercizio di averli studiati ti aiuterà a conoscere meglio ciò che è possibile con il tuo codice.
Carl Manaster,

6

Perché non includere una sorta di preprocessore di modelli \ generazione di codice per questo sistema? Un passaggio di creazione extra personalizzato che esegue ed emette file sorgente Java extra prima di compilare il resto del codice. Questo è il modo in cui spesso include l'inclusione di client Web fuori dai file wsdl e xsd.

Ovviamente avrai la manutenzione di quel preprocessore \ generatore di codice, ma non avrai la manutenzione duplicata del codice core di cui preoccuparti.

Poiché il tempo di compilazione viene emesso il codice Java, non vi è alcuna penalità di prestazione da pagare per il codice aggiuntivo. Ma ottieni la semplificazione della manutenzione avendo il modello invece di tutto il codice duplicato.

I generici Java non offrono alcun vantaggio in termini di prestazioni a causa della cancellazione del tipo nella lingua, al suo posto viene utilizzato il semplice casting.

Dalla mia comprensione dei modelli C ++, sono compilati in diverse funzioni per ogni invocazione di modello. Per esempio, finirai con un duplicato del timecode a metà compilazione per ogni tipo memorizzato in uno std :: vector.


2

Nessun metodo Java sano può essere abbastanza lungo da avere 12 varianti ... e JITC odia i metodi lunghi: rifiuta semplicemente di ottimizzarli correttamente. Ho visto un fattore di accelerazione di due semplicemente dividendo un metodo in due più brevi. Forse questa è la strada da percorrere.

OTOH con più copie può avere senso anche se ce ne fossero identiche. Man mano che ognuno di essi viene utilizzato in luoghi diversi, viene ottimizzato per casi diversi (il JITC li profila e posiziona i casi rari su un percorso eccezionale.

Direi che generare codice non è un grosso problema, supponendo che ci sia una buona ragione. L'overhead è piuttosto basso e i file correttamente denominati portano immediatamente alla loro fonte. Qualche tempo fa, quando ho generato il codice sorgente, ho inserito // DO NOT EDITogni riga ... Immagino che sia abbastanza salvifico.


1

Non c'è assolutamente nulla di sbagliato nel "problema" che hai citato. Da quello che so questo è il tipo esatto di progettazione e approccio utilizzato dal server DB per avere buone prestazioni.

Hanno molti metodi speciali per assicurarsi di poter massimizzare le prestazioni per tutti i tipi di operazioni: unire, selezionare, aggregare, ecc ... quando si applicano determinate condizioni.

In breve, la generazione di codice come quella che pensi sia una cattiva idea. Forse dovresti guardare questo diagramma per vedere come un DB risolve un problema simile al tuo:inserisci qui la descrizione dell'immagine


1

Si consiglia di verificare se è ancora possibile utilizzare alcune astrazioni sensibili e utilizzare, ad esempio, il modello di modello di modello per scrivere codice comprensibile per la funzionalità comune e spostare le differenze tra i metodi nelle "operazioni primitive" (secondo la descrizione del modello) in 12 sottoclassi. Ciò potrebbe migliorare la manutenibilità e la testabilità e potrebbe effettivamente avere le stesse prestazioni del codice corrente, dal momento che la JVM può incorporare le chiamate del metodo per le operazioni primitive dopo un po '. Ovviamente, devi verificarlo in un test delle prestazioni.


0

Puoi risolvere il problema dei metodi specializzati usando il linguaggio Scala.

Scala può incorporare metodi, questo (in combinazione con un facile utilizzo della funzione di ordine superiore) consente di evitare la duplicazione del codice gratuitamente - questo sembra il problema principale menzionato nella tua risposta.

Inoltre, Scala ha macro sintattiche, che rendono possibile fare molte cose con il codice in fase di compilazione in modo sicuro.

E il problema comune dei tipi primitivi di boxe quando usato in generici è anche possibile risolvere in Scala: può fare una specializzazione generica per i primitivi per evitare automaticamente il pugilato usando l' @specializedannotazione - questa cosa è incorporata nel linguaggio stesso. Quindi, fondamentalmente, scriverai un metodo generico in Scala, e funzionerà con la velocità di metodi specializzati. E se hai bisogno anche di aritmetiche generiche veloci, è facilmente realizzabile usando "pattern" di Typeclass per iniettare le operazioni e i valori per diversi tipi numerici.

Inoltre, Scala è fantastica in molti altri modi. E non devi riscrivere tutto il codice su Scala, perché l'interoperabilità di Scala <-> Java è eccellente. Assicurati di usare SBT (strumento di costruzione scala) per costruire il progetto.


-1

Se la funzione in questione è grande, può funzionare trasformando i bit "mode / paradigm" in un'interfaccia e quindi passare un oggetto che implementa tale interfaccia come parametro nella funzione. GoF chiama questo il modello "Strategia", iirc. Se la funzione è piccola, l'aumento delle spese generali potrebbe essere significativo ... qualcuno ha ancora menzionato la profilazione? ... più persone dovrebbero menzionare la profilazione.


questo sembra più un commento che una risposta
moscerino

-2

Quanti anni ha il programma? Molto probabilmente l'hardware più recente rimuoverà il collo di bottiglia e potresti passare a una versione più gestibile. Ma deve esserci un motivo per la manutenzione, quindi a meno che il tuo compito non sia migliorare la base di codice, lascia che funzioni.


1
questo sembra semplicemente ripetere i punti sollevati e spiegati in una risposta precedente pubblicata poche ore fa
moscerino

1
L'unico modo per determinare dove si trova effettivamente un collo di bottiglia è di profilarlo, come indicato nell'altra risposta. Senza queste informazioni chiave, qualsiasi soluzione suggerita per le prestazioni è pura speculazione.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.