Java lambdas 20 fois plus lent que les classes anonymes
J'ai vu beaucoup de questions à propos de Java lambdas de la performance, mais la plupart d'entre eux vont comme des "Lambdas sont légèrement plus rapide, mais plus lent lors de l'utilisation de fermetures" ou "Warm-up contre les temps d'exécution sont différentes", ou d'autres choses.
Cependant, j'ai frappé un peu étrange chose ici. Envisager cette LeetCode problème:
Donné un ensemble de non-chevauchement des intervalles, insérer un nouvel intervalle en
les intervalles (fusion si nécessaire).Vous pouvez supposer que les intervalles ont été d'abord triées en fonction de
leur temps de départ.
Le problème a été marqué dur, donc je suppose qu'une approche linéaire n'est pas ce qu'ils veulent là. J'ai donc décidé de venir avec un astucieux moyen de combiner la recherche binaire avec les modifications apportées à la liste d'entrée. Maintenant, le problème n'est pas très clair sur la modification de la liste d'entrée—il dit "insérer", même si la signature nécessite de retourner une référence à la liste, mais jamais l'esprit pour l'instant. Voici le code complet, mais seulement les premières lignes sont concernées par cette question. Je vais garder le reste ici simplement pour que tout le monde peut l'essayer:
public List<Interval> insert(List<Interval> intervals, Interval newInterval) {
int start = Collections.binarySearch(intervals, newInterval,
(i1, i2) -> Integer.compare(i1.start, i2.start));
int skip = start >= 0 ? start : -start - 1;
int end = Collections.binarySearch(intervals.subList(skip, intervals.size()),
new Interval(newInterval.end, 0),
(i1, i2) -> Integer.compare(i1.start, i2.start));
if (end >= 0) {
end += skip; //back to original indexes
} else {
end -= skip; //ditto
}
int newStart = newInterval.start;
int headEnd;
if (-start - 2 >= 0) {
Interval prev = intervals.get(-start - 2);
if (prev.end < newInterval.start) {
//the new interval doesn't overlap the one before the insertion point
headEnd = -start - 1;
} else {
newStart = prev.start;
headEnd = -start - 2;
}
} else if (start >= 0) {
//merge the first interval
headEnd = start;
} else { //start == -1, insertion point = 0
headEnd = 0;
}
int newEnd = newInterval.end;
int tailStart;
if (-end - 2 >= 0) {
//merge the end with the previous interval
newEnd = Math.max(newEnd, intervals.get(-end - 2).end);
tailStart = -end - 1;
} else if (end >= 0) {
newEnd = intervals.get(end).end;
tailStart = end + 1;
} else { //end == -1, insertion point = 0
tailStart = 0;
}
intervals.subList(headEnd, tailStart).clear();
intervals.add(headEnd, new Interval(newStart, newEnd));
return intervals;
}
Cela a bien fonctionné et j'ai été accepté, mais avec 80 ms de l'exécution, alors que la plupart des solutions étaient de 4 à 5 ms et certains 18-19 mme. Quand j'ai regardé en eux, ils étaient tous linéaire et très primitif. Pas quelque chose que l'on pourrait attendre d'un problème tagged "dur".
Mais ici se pose la question: ma solution est également linéaire au pire des cas (car ajouter/effacer des opérations sont en temps linéaire). Pourquoi est-il que plus lent? Et puis, j'ai fait ceci:
Comparator<Interval> comparator = new Comparator<Interval>() {
@Override
public int compare(Interval i1, Interval i2) {
return Integer.compare(i1.start, i2.start);
}
};
int start = Collections.binarySearch(intervals, newInterval, comparator);
int skip = start >= 0 ? start : -start - 1;
int end = Collections.binarySearch(intervals.subList(skip, intervals.size()),
new Interval(newInterval.end, 0),
comparator);
De 80 ms 4 ms! Ce qui se passe ici? Malheureusement je n'ai aucune idée de quel genre de tests LeetCode pistes ou en vertu de quel environnement, mais encore, n'est-ce pas 20 fois trop?
- Avez-vous à plusieurs reprises exécuter cette méthode et le temps mesuré?
- Voir stackoverflow.com/questions/504103/...
- Je n'ai pas accès pour le cas de test, donc je ne peux pas exécuter exactement à plusieurs reprises. Je ne sais pas ce LeetCode ne pour réaliser cette étrange performance, donc la question est: est-ce qu'il pourrait faire pour la rendre mauvaise?
- Si le code est exécuté qu'une seule fois (pour les exemples sur leetcode) la diminution du temps d'exécution peuvent être liés en raison du fait que le lambda bytecode est généré lors de l'exécution. Alors que votre anonyme comparateur de classe est créée lors de la compilation.
- Je pense que cela doit vraiment être la réponse, merci! Je n'avais aucune idée. Je pensais qu'elles étaient quelque chose comme du sucre syntaxique pour les classes.
- Jusqu'à présent, mon expérience anecdotique (à partir de mon propre cas d'utilisation), c'est que les lambdas sont juste plus lent que les classes anonymes. Cependant, comme quelques pour cent, pas tout 20x...
Vous devez vous connecter pour publier un commentaire.
Vous êtes évidemment de rencontrer les premiers temps de l'initialisation de la surcharge des expressions lambda. Comme déjà mentionné dans les commentaires, les classes pour les expressions lambda sont générés lors de l'exécution plutôt que d'être chargé à partir de votre chemin de classe.
Cependant, étant généré n'est pas la cause de ce ralentissement. Après tout, la génération d'une classe ayant une structure simple, peut être même plus rapide que le chargement de la même octets à partir d'une source externe. L'intérieur de la classe doit être trop chargé. Mais lorsque l'application n'a pas utilisé les expressions lambda avant, même le cadre de la génération de la lambda classes doit être chargé (Oracle actuel de la mise en œuvre utilise ASM sous le capot). Ceci est la cause réelle de ce ralentissement, le chargement et l'initialisation d'une douzaine utilisé en interne des classes, et non pas l'expression lambda lui-même.
Vous pouvez facilement vérifier cela. Dans votre code à l'aide des expressions lambda, vous avez deux expressions identiques
(i1, i2) -> Integer.compare(i1.start, i2.start)
. L'implémentation actuelle ne reconnaît pas cela (en fait, le compilateur ne fournit pas d'indice ni). Donc, ici, deux lambda cas, même les différentes classes, sont générés. Vous pouvez refactoriser le code pour avoir un seul de comparaison, similaire à l'intérieur de la classe variante:Vous ne remarquerez aucune différence de performances significative, comme il n'est pas le nombre d'expressions lambda que de questions, mais juste la classe de chargement et l'initialisation du cadre, ce qui se passe exactement une fois.
Vous pouvez même max en ajoutant d'autres expressions lambda comme
sans voir un ralentissement. C'est vraiment la charge initiale de la première lambda expression de l'ensemble de l'exécution que vous remarquez ici. Depuis Leetcode lui-même, apparemment, ne pas utiliser des expressions lambda avant d'entrer dans votre code, dont le temps d'exécution est mesuré, cette charge s'ajoute à l'exécution de votre temps ici.
Voir aussi “Comment Java lambda fonctions compilées?” et “Une expression lambda de créer un objet sur le tas à chaque fois qu'il est exécuté?”
--module-path
sur la ligne de commande le faire disparaître; apparemment, le code de la manipulation des modules de l'application fait l'utilisation des expressions lambda.Nashhorn
; nous avons beaucoup de code qui comptaient sur elle, mais à cause deinvokedynamic
utilisé, et être assez lent pour la première va (dans notre cas, dans certains cas, il était de 25 à 30 s) on a laissé tomber il y a quelques années. Peut-être le temps de tester cette contre java-11, mais aucune idée si quelqu'un aurait envie de ré-écrire ces portions de code si