2016-06-08 19 views
6

Al mio lavoro abbiamo una DSL per specfying formule matematiche, che in seguito applicheremo a molti punti (in milioni).MethodHandles o LambdaMetafactory?

A partire da oggi, costruiamo un AST della formula e visitiamo ciascun nodo per produrre quello che chiamiamo un "Valutatore". Passiamo quindi a quel valutatore gli argomenti della formula, e per ogni punto fa il calcolo.

Ad esempio, si ha che la formula: x * (3 + y)

  ┌────┐ 
    ┌─────┤mult├─────┐ 
    │  └────┘  │ 
    │    │ 
    ┌──v──┐   ┌──v──┐ 
    │ x │  ┌───┤ add ├──┐ 
    └─────┘  │ └─────┘ │ 
       │   │ 
      ┌──v──┐  ┌──v──┐ 
      │ 3 │  │ y │ 
      └─────┘  └─────┘ 

nostro valutatore emetterà "Valutare" oggetti per ogni passo.

Questo metodo è facile da programmare, ma non molto efficiente.

Così ho iniziato a esaminare le maniglie del metodo per creare un handle del metodo "composto" per accelerare le cose ultimamente.

Qualcosa lungo questo: io ho la mia classe "aritmetica" con:

public class Arithmetics { 

    public static double add(double a, double b){ 
     return a+b; 
    } 

    public static double mult(double a, double b){ 
     return a*b; 
    } 

} 

E quando costruire la mia AST io uso MethodHandles.lookup() per ottenere direttamente una maniglia su quelli e li compongono. Qualcosa su questa linea, ma su un albero:

Method add = ArithmeticOperator.class.getDeclaredMethod("add", double.class, double.class); 
Method mult = ArithmeticOperator.class.getDeclaredMethod("mult", double.class, double.class); 
MethodHandle mh_add = lookup.unreflect(add); 
MethodHandle mh_mult = lookup.unreflect(mult); 
MethodHandle mh_add_3 = MethodHandles.insertArguments(mh_add, 3, plus_arg); 
MethodHandle formula = MethodHandles.collectArguments(mh_mult, 1, mh_add_3); // formula is f(x,y) = x * (3 + y) 

Purtroppo, sono piuttosto deluso dai risultati. Ad esempio, la costruzione effettiva dell'handle del metodo è molto lunga (a causa delle chiamate a MethodHandles :: insertArguments e ad altre funzioni di tali composizioni) e l'accelerazione aggiunta per la valutazione inizia a fare la differenza dopo oltre 600k di iterazioni.

A 10 milioni di iterazioni, il metodo Handle inizia a brillare veramente, ma milioni di iterazioni non sono (ancora?) Un tipico caso d'uso. Siamo più vicini a 10k-1M, dove il risultato è misto.

Inoltre, il calcolo effettivo è accelerato, ma non così tanto (~ 2-10 volte). Mi aspettavo la cosa di eseguire un po 'più veloce ..

Quindi, comunque, ho iniziato di nuovo purga StackOverflow, e vide i fili LambdaMetafactory come questi: https://stackoverflow.com/a/19563000/389405

E sto prurito per iniziare a provare questo. Ma prima di ciò, vorrei il vostro input su alcune domande:

  • Devo essere in grado di comporre tutti quei lambda. MethodHandles offre molti modi (lenti, ammisamente) per farlo, ma mi sento come se lambda avesse una "interfaccia" più rigida, e non riesco ancora a spiegarmi come farlo. Sai come?

  • lambda e le maniglie del metodo sono piuttosto interconnesse e non sono sicuro che otterrò una significativa accelerazione. Vedo questi risultati per semplici lambda: direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40 ma per quanto riguarda i lambda composti?

Grazie ragazzi!

+1

C'è un componente dinamico che suggerisce tali approcci? Un albero ordinario di oggetti (preferibilmente immutabili) non è poi così male. – Holger

+0

Il nostro albero di oggetti utilizza effettivamente un'interfaccia e abbiamo discusso il costo di tutto il dispatch dinamico e abbiamo pensato che avremmo probabilmente ottenuto un handle di metodo personalizzato per questo. I miei test dimostrano che questo è il caso di grandi alberi o con molte iterazioni per piccoli alberi. Ora mi chiedo se lambdas potrebbe migliorare un po 'di più – Gui13

+1

Hai eseguito benchmark effettivi sul "costo di tutta la spedizione dinamica"? Se hai un albero immutabile, HotSpot è in genere molto bravo a fare inline aggressive. E dovrai fare affidamento su questa capacità anche quando usi lambdas, dato che sono progettati anche attorno alle interfacce. – Holger

risposta

4

Penso che, per la maggior parte dei casi pratici, un albero di valutazione immutabile costituito da nodi che soddisfano un'interfaccia particolare o che erediti da una classe di valutazione comune, è imbattibile.HotSpot è in grado di eseguire l'inlining (aggressivo), almeno per i sottoalberi, ma ha la libertà di decidere quanti nodi sarà in linea.

Al contrario, la generazione di codice esplicito per un intero albero, impone il rischio di superare le soglie della JVM, quindi, si dispone di un codice che non ha un overhead di spedizione, ma che potrebbe essere interpretato ininterrottamente.

Un albero di MethodHandle s adattato si avvia come qualsiasi altro albero, ma con un sovraccarico maggiore. È discutibile se la propria ottimizzazione sia in grado di battere la strategia di inlining propria di HotSpots. E come hai notato, ci vuole un lungo numero di invocazioni, prima che si attivi l'autoregolazione. Sembra che le soglie si accumulino in un modo sfortunato per le maniglie del metodo composto.

Per citare un esempio prominente del modello dell'albero di valutazione, quando si utilizza Pattern.compile per preparare un'operazione di corrispondenza regolare, non verrà generato codice bytecode o codice nativo, nonostante il nome del metodo possa indurre in errore a pensare in quella direzione. La rappresentazione interna è solo un immutabile albero di nodi, che rappresenta le combinazioni dei diversi tipi di operazioni. Spetta all'ottimizzatore JVMs generare codice appiattito per esso dove è considerato utile.

Le espressioni lambda non cambiano il gioco. Ti consentono di generare classi (piccole) che soddisfano un'interfaccia e invocano un metodo di destinazione. Si possono utilizzare per costruire un albero di valutazione immutabile, e mentre questo è improbabile che abbia un andamento diverso da quello esplicitamente programmato classi nodo di valutazione, che consente al codice molto più semplice:

public class Arithmetics { 
    public static void main(String[] args) { 
     // x * (3 + y) 
     DoubleBinaryOperator func=op(MUL, X, op(ADD, constant(3), Y)); 
     System.out.println(func.applyAsDouble(5, 4)); 
     PREDEFINED_UNARY_FUNCTIONS.forEach((name, f) -> 
      System.out.println(name+"(0.42) = "+f.applyAsDouble(0.42))); 
     PREDEFINED_BINARY_FUNCTIONS.forEach((name, f) -> 
      System.out.println(name+"(0.42,0.815) = "+f.applyAsDouble(0.42,0.815))); 
     // sin(x)+cos(y) 
     func=op(ADD, 
      op(PREDEFINED_UNARY_FUNCTIONS.get("sin"), X), 
      op(PREDEFINED_UNARY_FUNCTIONS.get("cos"), Y)); 
     System.out.println("sin(0.6)+cos(y) = "+func.applyAsDouble(0.6, 0.5)); 
    } 
    public static DoubleBinaryOperator ADD = Double::sum; 
    public static DoubleBinaryOperator SUB = (a,b) -> a-b; 
    public static DoubleBinaryOperator MUL = (a,b) -> a*b; 
    public static DoubleBinaryOperator DIV = (a,b) -> a/b; 
    public static DoubleBinaryOperator REM = (a,b) -> a%b; 

    public static <T> DoubleBinaryOperator op(
     DoubleUnaryOperator op, DoubleBinaryOperator arg1) { 
     return (x,y) -> op.applyAsDouble(arg1.applyAsDouble(x,y)); 
    } 
    public static DoubleBinaryOperator op(
     DoubleBinaryOperator op, DoubleBinaryOperator arg1, DoubleBinaryOperator arg2) { 
     return (x,y)->op.applyAsDouble(arg1.applyAsDouble(x,y),arg2.applyAsDouble(x,y)); 
    } 
    public static DoubleBinaryOperator X = (x,y) -> x, Y = (x,y) -> y; 
    public static DoubleBinaryOperator constant(double value) { 
     return (x,y) -> value; 
    } 

    public static final Map<String,DoubleUnaryOperator> PREDEFINED_UNARY_FUNCTIONS 
     = getPredefinedFunctions(DoubleUnaryOperator.class, 
      MethodType.methodType(double.class, double.class)); 
    public static final Map<String,DoubleBinaryOperator> PREDEFINED_BINARY_FUNCTIONS 
     = getPredefinedFunctions(DoubleBinaryOperator.class, 
      MethodType.methodType(double.class, double.class, double.class)); 

    private static <T> Map<String,T> getPredefinedFunctions(Class<T> t, MethodType mt) { 
     Map<String,T> result=new HashMap<>(); 
     MethodHandles.Lookup l=MethodHandles.lookup(); 
     for(Method m:Math.class.getMethods()) try { 
      MethodHandle mh=l.unreflect(m); 
      if(!mh.type().equals(mt)) continue; 
      result.put(m.getName(), t.cast(LambdaMetafactory.metafactory(
      MethodHandles.lookup(), "applyAsDouble", MethodType.methodType(t), 
      mt, mh, mt) .getTarget().invoke())); 
     } 
     catch(RuntimeException|Error ex) { throw ex; } 
     catch(Throwable ex) { throw new AssertionError(ex); } 
     return Collections.unmodifiableMap(result); 
    } 
} 

Questo è tutto ciò che serve per comporre valutatori per le espressioni fatto di operatori aritmetici di base e funzioni trovate in java.lang.Math, quest'ultimo raccolto dinamicamente, per affrontare quell'aspetto della tua domanda.

noti che tecnicamente,

public static DoubleBinaryOperator MUL = (a,b) -> a*b; 

è solo una breve mano per

public static DoubleBinaryOperator MUL = Arithmetics::mul; 
public static double mul(double a, double b){ 
    return a*b; 
} 

ho aggiunto un metodo main contenente alcuni esempi. Tieni presente che queste funzioni si comportano come codice compilato, proprio nella prima invocazione, poiché in realtà consistono solo di codice compilato, ma composto da più funzioni.

+0

Questa è una buona risposta, grazie Holger. Ho ancora delle aree grigie (come avere una quantità variabile di parametri), ma è un buon inizio. – Gui13