41 votes

Comment puis-je coder Java pour permettre l'utilisation de SSE et l'élimination de bounds-check (ou d'autres optimisations avancées)?

La Situation:

Je suis de l'optimisation d'un pur-java mise en œuvre de la LZF algorithme de compression, ce qui implique beaucoup de byte[] d'accès et de base int mathématiques pour le hachage et la comparaison. La Performance est vraiment important, parce que le but de la compression est de réduire les exigences d'e/S. Je ne suis pas poster du code, car il n'est pas nettoyé encore, et peut-être restructuré fortement.

Les Questions:

  • Comment puis-je écrire mon code afin de permettre à JIT compiler un formulaire à l'aide de vite SSE opérations?
  • Comment puis-je de la structure de sorte que le compilateur peut facilement éliminer les limites du tableau des contrôles?
  • Existe-il des grandes références au sujet de la vitesse relative de certaines opérations mathématiques (combien de incrémente/décrémente faut-il pour l'égalité normale ajouter/soustraire, quelle est la vitesse de changement-ou contre un tableau)?
  • Comment puis-je travailler sur l'optimisation de la ramification -- est-il préférable d'avoir de nombreuses instructions conditionnelles avec de courtes corps, ou un peu longs, ou courts, avec des conditions nichées?
  • Avec 1,6 JVM, combien d'éléments doivent être copiés à l'avant du Système.arraycopy bat une copie de la boucle?

Ce que j'ai déjà fait:

Avant de me faire attaquer pour l'optimisation prématurée: l'algorithme de base est déjà excellente, mais l'implémentation de Java est inférieure à 2/3 de la vitesse de l'équivalent de C. j'ai déjà remplacé la copie des boucles avec le Système.arraycopy, a travaillé sur l'optimisation de boucles et éliminé de l'onu nécessaire des opérations.

Je fais un usage intensif de peu tourner et d'emballage octets dans ints de performances, ainsi que de changer & de masquage.

Pour des raisons juridiques, je ne peux pas regarder mises en œuvre dans les bibliothèques similaires, et les bibliothèques ont trop restrictive les conditions de licence pour l'utiliser.

Exigences pour une BONNE (accepté) réponse:

  • Inacceptable répond: "c'est plus rapide", sans une explication de combien ET pourquoi, OU n'a pas été testé avec un compilateur JIT.
  • Limite des réponses: n'ont pas été testés avec quoi que ce soit avant Hotspot 1.4
  • Des réponses simples: fournira une règle générale et que l'explication de pourquoi il est plus rapide au niveau du compilateur, et à peu près combien plus rapide
  • Bonnes réponses: inclure un couple d'échantillons de code pour illustrer
  • Excellentes réponses: avoir des repères à la fois avec le JRE 1.5 et 1.6
  • PARFAIT réponse: Est par quelqu'un qui a travaillé sur le HotSpot compilateur, et peut expliquer ou de référence, les conditions d'une optimisation à être utilisé, et comment beaucoup plus rapide, il est généralement. Peut inclure du code java et exemples de code assembleur généré par HotSpot.

Aussi: si quelqu'un a des liens détaillant les entrailles de Hotspot de l'optimisation et de la ramification de la performance, ceux sont les bienvenus. J'en sais assez sur le bytecode qu'un site d'analyse de la performance à un bytecode plutôt que le code source serait utile.

(Edit) Réponse Partielle: Limites-Vérifier Ellimination:

Ceci est pris à partir fournis lien vers le HotSpot internes au wiki à: https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination

HotSpot permettra d'éliminer les limites des contrôles dans tous les cas pour les boucles avec les conditions suivantes:

  • Tableau est invariant de boucle (pas réaffecter à l'intérieur de la boucle)
  • L'indice de la variable est une constante de la foulée (augmente/diminue en quantité constante, en un seul endroit, si possible)
  • Tableau est indexé par une fonction linéaire de la variable.

Exemple: int val = array[index*2 + 5]

OU: int val = array[index+9

PAS: int val = array[Math.min(var,index)+7]


La première version du code:

Ceci est un exemple de version. Ne pas voler, parce que c'est une version éditée de code pour le H2 projet de base de données. La version finale sera open source. C'est une optimisation sur le code ici: H2 CompressLZF code

Logiquement, c'est identique à la version de développement, mais que l'on utilise un pour(...) de la boucle à l'étape d'entrée, et d'un si/d'autre de la boucle pour des logiques différentes entre littéral et backreference modes. Il réduit l'accès au tableau et vérifie entre les modes.

public int compressNewer(final byte[] in, final int inLen, final byte[] out, int outPos){
        int inPos = 0;
        // initialize the hash table
        if (cachedHashTable == null) {
            cachedHashTable = new int[HASH_SIZE];
        } else {
            System.arraycopy(EMPTY, 0, cachedHashTable, 0, HASH_SIZE);
        }
        int[] hashTab = cachedHashTable;
        // number of literals in current run
        int literals = 0;
        int future = first(in, inPos);
        final int endPos = inLen-4;

        // Loop through data until all of it has been compressed
        while (inPos < endPos) {
                future = (future << 8) | in[inPos+2] & 255;
//                hash = next(hash,in,inPos);
                int off = hash(future);
                // ref = possible index of matching group in data
                int ref = hashTab[off];
                hashTab[off] = inPos;
                off = inPos - ref - 1; //dropped for speed

                // has match if bytes at ref match bytes in future, etc
                // note: using ref++ rather than ref+1, ref+2, etc is about 15% faster
                boolean hasMatch = (ref > 0 && off <= MAX_OFF && (in[ref++] == (byte) (future >> 16) && in[ref++] == (byte)(future >> 8) && in[ref] == (byte)future));

                ref -=2; // ...EVEN when I have to recover it
                // write out literals, if max literals reached, OR has a match
                if ((hasMatch && literals != 0) || (literals == MAX_LITERAL)) {
                    out[outPos++] = (byte) (literals - 1);
                    System.arraycopy(in, inPos - literals, out, outPos, literals);
                    outPos += literals;
                    literals = 0;
                }

                //literal copying split because this improved performance by 5%

                if (hasMatch) { // grow match as much as possible
                    int maxLen = inLen - inPos - 2;
                    maxLen = maxLen > MAX_REF ? MAX_REF : maxLen;
                    int len = 3;
                    // grow match length as possible...
                    while (len < maxLen && in[ref + len] == in[inPos + len]) {
                        len++;
                    }
                    len -= 2;

                    // short matches write length to first byte, longer write to 2nd too
                    if (len < 7) {
                        out[outPos++] = (byte) ((off >> 8) + (len << 5));
                    } else {
                        out[outPos++] = (byte) ((off >> 8) + (7 << 5));
                        out[outPos++] = (byte) (len - 7);
                    }
                    out[outPos++] = (byte) off;
                    inPos += len;

                    //OPTIMIZATION: don't store hashtable entry for last byte of match and next byte
                    // rebuild neighborhood for hashing, but don't store location for this 3-byte group
                    // improves compress performance by ~10% or more, sacrificing ~2% compression...
                    future = ((in[inPos+1] & 255) << 16) | ((in[inPos + 2] & 255) << 8) | (in[inPos + 3] & 255);
                    inPos += 2;
                } else { //grow literals
                    literals++;
                    inPos++;
                } 
        }

        // write out remaining literals
        literals += inLen-inPos;
        inPos = inLen-literals;
        if(literals >= MAX_LITERAL){
            out[outPos++] = (byte)(MAX_LITERAL-1);
            System.arraycopy(in, inPos, out, outPos, MAX_LITERAL);
            outPos += MAX_LITERAL;
            inPos += MAX_LITERAL;
            literals -= MAX_LITERAL;
        }
        if (literals != 0) {
            out[outPos++] = (byte) (literals - 1);
            System.arraycopy(in, inPos, out, outPos, literals);
            outPos += literals;
        }
        return outPos; 
    }

Montage Final:

J'ai marqué la meilleure réponse jusqu'à présent comme acceptée, car la date limite est presque à la fin. Depuis que j'ai pris autant de temps avant de décider de poster le code, je vais continuer à upvote et répondre à des commentaires si possible. Toutes mes excuses si le code est illisible, c'està-code en développement, n'est pas poli de s'engager.

19voto

ShuggyCoUk Points 24204

Pas une réponse complète, je n'ai tout simplement pas le temps pour faire le détail des repères à votre question, mais je l'espère utile.

Connais ton ennemi

Vous ciblez une combinaison de la JVM (dans l'essence de l'JIT) et le sous-jacent CPU/sous-système de Mémoire. Ainsi, "C'est plus rapide sur la JVM X" n'est pas susceptible d'être valable dans tous les cas, que vous vous déplacez en plus agressive des optimisations.

Si votre marché cible/demande, pour la plupart, sur une architecture particulière, vous devriez envisager d'investir dans des outils spécifiques. * Si vos performances sur x86 est le facteur essentiel, puis intel VTune est excellent pour le forage vers le bas dans le genre de jit analyse des résultats que vous décrivez. * Les différences entre 64 bits et 32 bits de l'équipe commune d'enquête peuvent être considérables, en particulier sur les plates-formes x86, où les conventions d'appel peut modifier et enregistering les possibilités sont très différentes.

Obtenir les bons outils

Vous souhaitez obtenir un profileur d'échantillonnage. La surcharge de l'instrumentation et de la frapper sur des choses comme l'in-lining, la pollution du cache et de la taille du code de l'inflation) pour vos besoins spécifiques serait beaucoup trop grande. Intel VTune analyseur peut être utilisé pour Java si l'intégration n'est pas si serré que les autres.
Si vous utilisez la JVM de sun et sont heureux que de savoir ce que la dernière version est à faire, puis les options disponibles pour enquêter sur la sortie de l'équipe sont importants si vous connaissez un peu l'assemblée. Cet article les détails intéressants de l'analyse à l'aide de cette fonctionnalité

Apprendre des autres implémentations

Le Changement d'histoire de changer l'histoire indique que les précédentes assembly en ligne est en fait contre-productif et qu'en permettant au compilateur de prendre le contrôle total de la production (avec les réglages dans le code plutôt que des directives de l'assemblée) ont donné de meilleurs résultats.

Quelques précisions

Depuis LZF est une réponse efficace non géré la mise en œuvre sur un ordinateur de bureau moderne PROCESSEURS, en grande partie de la mémoire de la bande passante limitée (d'où qu'il soit compered à la vitesse d'un unoptimised memcpy), vous devrez vous code restent entièrement à l'intérieur de niveau 1 cache.
Comme ces champs statiques vous ne pouvez pas vous rendre dans les constantes doivent être placés dans la même classe que ces valeurs vont souvent être placés dans la même zone de mémoire consacré à la vtables et les méta-données associées aux classes.

Les allocations d'objets qui ne peuvent pas être piégé par Échapper à l'Analyse (uniquement en 1.6 à partir) doivent être évitées.

Le code c rend l'utilisation agressive de déroulement de la boucle. Toutefois, la performance de ce sur plus (1,4 ère) VM est fortement dépendante du mode de la JVM est en. Apparemment, ce dernier jvm de sun versions sont plus agressifs à l'in-lining et de dérouler, en particulier en mode serveur.

Le prefetch instrctions généré par le JIT peut faire toute la différence sur le code à l'instar de ce qui est à proximité lié à la mémoire.

"Elle arrive tout droit sur nous"

Votre cible est en mouvement, et continuera à. De nouveau Marc Lehmann de l'expérience précédente:

par défaut HLOG taille est maintenant de 15 (caches cpu ont augmenté)

Même les mises à jour mineures à la jvm peut impliquer d'importants changements compilateur

6544668 Ne pas vecorized tableau des opérations qui ne peuvent pas être alignés lors de l'exécution. 6536652 mettre en Œuvre certaines superword (SIMD) optimisations 6531696 n'utilisez pas immédiate de 16 bits de la valeur mémorisée sur les processeurs Intel 6468290 Diviser et répartir de eden sur une base par cpu

Le Capitaine Évident

Mesurer, Mesurer, Mesurer. SI vous pouvez obtenir votre bibliothèque à inclure (dans une dll) une solution simple et facile à exécuter de référence qui enregistre les informations pertinentes (vm version, processeur, système d'exploitation, commutateurs de ligne de commande etc) et fait de cette simple d'envoyer de nouveau à vous, vous permettra d'augmenter votre couverture, le meilleur de tous, vous allez couvrir ces personnes à l'aide de ce que des soins.

6voto

João Silva Points 36619

Aussi loin que les limites de vérifier l'élimination est concerné, je crois que le nouveau JDK déjà une amélioration de l'algorithme qui l'élimine, chaque fois que c'est possible. Ce sont les deux principaux articles sur ce sujet:

  • V. Mikheev, S. Fedoseev, V. Sukharev, N. M. Lipsky. 2002 Efficace d'Amélioration de la Boucle de gestion des versions de Java. Lien. Ce document est à partir du gars à l'Excelsior, qui a mis en œuvre la technique dans leur Jet de la JVM.
  • Würthinger, Thomas, Christian Wimmer, et Hanspeter Mössenböck. 2007. Les Limites du tableau de Vérifier l'Élimination de la Java HotSpot Client de Compilateur. PPPJ. Lien. Légèrement basé sur le document cité ci-dessus, c'est la mise en œuvre qui, je crois, sera inclus dans la prochaine version de JDK. Le accélérations sont également présentés.

Il y a aussi cette entrée de blog, qui traite de l'un des documents superficiellement, et présente certains résultats de l'analyse comparative, non seulement pour les tableaux, mais aussi pour l'arithmétique dans le nouveau JDK. Les commentaires de l'entrée de blog sont aussi très intéressants, puisque les auteurs des articles ci-dessus présente de très intéressants commentaires et de discuter des arguments. Aussi, il y a certains points à d'autres messages de blog sur ce sujet.

Espérons que cela aide.

3voto

Michael Borgwardt Points 181658

Il est peu probable que vous avez besoin pour aider le compilateur JIT beaucoup avec l'optimisation d'un simple calcul de l'algorithme comme LZW. ShuggyCoUk parlé, mais je pense qu'il mérite une attention particulière:

Le cache de la convivialité de votre code sera un grand facteur.

Vous devez réduire la taille de votre woking définir et à améliorer l'accès aux données de la localité autant que possible. Vous parlez de "l'emballage octets dans ints pour la performance". Cela ressemble à l'aide d'ints de tenir valeurs d'octets afin d'avoir leur mot-alignés. Ne le faites pas! L'augmentation de l'ensemble de données de taille emportent sur les éventuels gains (une fois, j'ai converti quelques-uns ECC numéro de croquer le code de type int[] pour byte[] et a obtenu une vitesse 2x-up).

A tout hasard que vous ne le connaissez pas: si vous avez besoin pour traiter certaines données comme les deux octets et ints, vous n'avez pas à changer et |masque-il utiliser ByteBuffer.asIntBuffer() et les méthodes connexes.

Avec 1,6 JVM, combien de les éléments doivent être copiés avant Système.arraycopy bat une copie de la boucle?

Mieux faire le test vous-même. Quand je l'ai fait le chemin du retour lorsque, en Java 1.3 fois, c'était quelque part autour de 2000 éléments.

2voto

StaxMan Points 34626

Beaucoup de réponses jusqu'à présent, mais quelques autres choses:

  • Mesurer, mesurer, mesurer. Autant que la plupart des développeurs Java en garde contre les micro-analyse comparative, assurez-vous de comparer les performances entre les changements. Les optimisations qui n'aboutissent pas à des améliorations mesurables ne sont généralement pas la peine de garder (bien sûr, parfois, il est une combinaison de choses, et c'est plus difficile)
  • Boucles serrées autant d'importance avec le Java, le C (et idem avec attribution de variables -- bien que vous ne contrôlez pas directement, il, HotSpot finira par avoir à le faire). J'arrive à pratiquement le double de la vitesse de l'UTF-8 décodage par la réorganisation de boucle pour la manipulation d'un octet cas (ascii 7 bits) aussi serré(re) boucle intérieure, laissant les autres cas.
  • Ne pas sous-estimer le coût de l'octroi et/ou de compensation des tableaux de grande taille -- si vous voulez lzf de codage/décodage d'être plus rapide pour les petites/moyennes morceaux trop (et pas seulement mégaoctet de taille moyenne), gardez à l'esprit que TOUTES les allocations de byte[]/int[] sont un peu cher, non pas parce que de GC, mais parce que la JVM DOIT dégager l'espace.

H2 mise en œuvre a également été optimisé un peu (par exemple: il n'est pas clair du tableau de hachage plus souvent, cela fait sens), et j'ai effectivement contribué à modifier pour l'utiliser dans un autre projet Java. Ma contribution a été la plupart du temps simplement en changeant il ne sera plus optimale pour les non-streaming cas, mais qui ne touchent pas le serré encoder/décoder des boucles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X