37 votes

Eigen: effet du style de codage sur les performances

De ce que j'ai lu sur Eigen (ici), il semble qu' operator=() agit comme une "barrière" de toutes sortes pour les paresseux de l'évaluation-par exemple, il provoque Propres à arrêter de retour de l'expression des modèles et de la performance de l' (optimisé) le calcul, le stockage du résultat dans la partie gauche de l' =.

Cela semble signifier que le "style de codage" a un impact sur les performances -- c'est à dire en utilisant les variables nommées pour stocker le résultat de calculs intermédiaires pourrait avoir un effet négatif sur la performance de l'origine de certaines parties du calcul pour être évalué "trop tôt".

Pour tenter de vérifier mon intuition, j'ai écrit un exemple et a été surpris par les résultats (code complet ici):

using ArrayXf  = Eigen::Array <float, Eigen::Dynamic, Eigen::Dynamic>;
using ArrayXcf = Eigen::Array <std::complex<float>, Eigen::Dynamic, Eigen::Dynamic>;

float test1( const MatrixXcf & mat )
{
    ArrayXcf arr  = mat.array();
    ArrayXcf conj = arr.conjugate();
    ArrayXcf magc = arr * conj;
    ArrayXf  mag  = magc.real();
    return mag.sum();
}

float test2( const MatrixXcf & mat )
{
    return ( mat.array() * mat.array().conjugate() ).real().sum();
}

float test3( const MatrixXcf & mat )
{
    ArrayXcf magc   = ( mat.array() * mat.array().conjugate() );

    ArrayXf mag     = magc.real();
    return mag.sum();
}

Le ci-dessus donne 3 différentes manières de calculer le coefficient de sage somme de grandeurs dans une valeur complexe de la matrice.

  1. test1 de tri de chaque partie du calcul "une étape à la fois."
  2. test2 le calcul en une seule expression.
  3. test3 prend un "mélange" approche-avec une certaine quantité de variables intermédiaires.

J'ai attendu que, depuis test2 packs de la totalité de calcul en une seule expression, Eigen serait en mesure de prendre avantage de ce mondial et d'optimiser l'ensemble de calcul, offrant les meilleures performances.

Cependant, les résultats sont surprenants (les chiffres indiqués sont en total microsecondes à travers 1000 exécutions de chaque test):

test1_us: 154994
test2_us: 365231
test3_us: 36613

(Cela a été compilé avec g++ -O3 -- voir l'essentiel pour plus de détails.)

La version que j'ai prévu pour être le plus rapide (test2) était en fait plus lent. En outre, la version que je m'attendais à être plus lent (test1) était en fait dans le milieu.

Donc, mes questions sont les suivantes:

  1. Pourquoi est - test3 effectuer beaucoup mieux que les autres?
  2. Est-il une technique que l'on peut utiliser (à court de plonger dans le code assembleur) pour obtenir une certaine visibilité de la façon dont Eigen est effectivement mise en œuvre de vos calculs?
  3. Est-il un ensemble de lignes directrices à suivre pour trouver un bon compromis entre les performances et la lisibilité (utilisation de variables intermédiaires) dans votre Propre code?

En plus de calculs complexes, tout en un seul expression pourrait nuire à la lisibilité, donc je suis intéressé à trouver la bonne façon d'écrire le code qui est à la fois lisible et performant.

15voto

kangshiyin Points 8571

Il ressemble à un problème de GCC. Intel compilateur donne le résultat attendu.

$ g++ -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a
test1_us: 200087
test2_us: 320033
test3_us: 44539

$ icpc -I ~/program/include/eigen3 -std=c++11 -O3 a.cpp -o a && ./a
test1_us: 214537
test2_us: 23022
test3_us: 42099

Par rapport à l' icpc version, gcc semble avoir problème de l'optimisation de votre test2.

Pour un résultat plus précis, vous souhaiterez peut-être désactiver le débogage des assertions en -DNDEBUG comme indiqué ici.

MODIFIER

Pour la question 1

@ggael donne une excellente réponse qu' gcc d'échec de la vectorisation de la somme de la boucle. Mon expérience aussi trouver qu' test2 est aussi rapide que la main écrit naïf pour boucle, à la fois avec gcc et icc, ce qui suggère que la vectorisation est la raison, et non temporaire allocation de mémoire est détectée en test2 par la méthode mentionnée ci-dessous, ce qui suggère que Eigen évaluer l'expression correctement.

Pour la question 2

En évitant les intermédiaires de la mémoire est l'objectif principal de Eigen l'utilisation de l'expression des modèles. Donc Eigen fournit une macro EIGEN_RUNTIME_NO_MALLOC et une fonction simple pour vous permettre de vérifier si un intermédiaire de la mémoire est allouée au calcul de l'expression. Vous pouvez trouver un exemple de code ici. Veuillez noter que cela peut fonctionner uniquement en mode de débogage.

EIGEN_RUNTIME_NO_MALLOC - si définie, un nouveau commutateur est mis en place, qui peut être activé et désactivé en appelant set_is_malloc_allowed(bool). Si malloc n'est pas autorisé et modes Propres tente d'allouer de la mémoire dynamiquement de toute façon, un échec d'assertion résultats. Non défini par défaut.

Pour la question 3

Il y a un moyen d'utiliser des variables intermédiaires, et pour obtenir l'amélioration de la performance introduite par l'évaluation différée/expression des modèles en même temps.

La façon est d'utiliser des variables intermédiaires avec le type de données correct. Au lieu d'utiliser Eigen::Matrix/Array, ce qui indique à l'évaluation de l'expression, vous devez utiliser le type d'expression Eigen::MatrixBase/ArrayBase/DenseBase de sorte que l'expression n'est tamponnée mais pas évaluée. Cela signifie que vous devez stocker l'expression en tant qu'intermédiaire, plutôt que le résultat de l'expression, à la condition que cet intermédiaire va être utilisé une seule fois dans le code suivant.

Que de déterminer les paramètres du modèle dans le type d'expression Eigen::MatrixBase/... pourrait être douloureux, vous pouvez utiliser auto à la place. Vous pourrez trouver quelques indices sur le moment vous devrait/ne devrait pas utiliser auto/types d'expression dans cette page. Une autre page vous indique également comment passer les expressions que les paramètres de la fonction sans les évaluer.

Selon le instructive expérience sur .abs2() dans @ggael 's réponse, je pense qu'un autre recommandation est d'éviter de réinventer la roue.

14voto

ggael Points 18968

Ce qui se passe est que, en raison de l' .real() étape, Eigen ne sera pas explicitement vectoriser test2. Il en appelle donc à la norme complexe::operator* l'opérateur, qui, malheureusement, n'est jamais inline par gcc. Les autres versions, d'autre part, les utilisations Propres propres vectorisé mise en œuvre de produits de complexes.

En revanche, la CPI n'inline complexe::operator*, et donc de faire de l' test2 le plus rapide pour la CPI. Vous pouvez également réécrire test2 comme:

return mat.array().abs2().sum();

pour obtenir de meilleures performances, même sur tous les compilateurs:

gcc:
test1_us: 66016
test2_us: 26654
test3_us: 34814

icpc:
test1_us: 87225
test2_us: 8274
test3_us: 44598

clang:
test1_us: 87543
test2_us: 26891
test3_us: 44617

Le très bon score de la CPI dans ce cas est due à son astucieux auto-vectorisation du moteur.

Une autre façon de contourner l'inlining échec de la gcc sans modifiant test2 est de définir votre propre operator* pour complex<float>. Par exemple, ajoutez la ligne suivante au début de votre fichier:

namespace std {
  complex<float> operator*(const complex<float> &a, const complex<float> &b) {
    return complex<float>(real(a)*real(b) - imag(a)*imag(b), imag(a)*real(b) + real(a)*imag(b));
  }
}

puis-je obtenir:

gcc:
test1_us: 69352
test2_us: 28171
test3_us: 36501

icpc:
test1_us: 93810
test2_us: 11350
test3_us: 51007

clang:
test1_us: 83138
test2_us: 26206
test3_us: 45224

Bien sûr, cette astuce n'est pas toujours recommandé, car, contrairement à la glib version, elle pourrait conduire à débordement ou numérique annulation des questions, mais ce que l'icpc et l'autre vectorisé versions de calcul, de toute façon.

5voto

mindriot Points 41

Une chose que j'ai fait avant, c'est de profiter de l' auto mot clé de beaucoup de choses. En gardant à l'esprit que la plupart des Propres expressions de retour expression particulière types de données (par exemple, CwiseBinaryOp), l'attribution d'un retour à un Matrix peut forcer l'évaluation de l'expression (qui est ce que vous voyez). À l'aide de auto permet au compilateur de déduire le type de retour que n'importe quel type d'expression il est, ce qui permettra d'éviter d'évaluation le plus longtemps possible:

float test1( const MatrixXcf & mat )
{
    auto arr  = mat.array();
    auto conj = arr.conjugate();
    auto magc = arr * conj;
    auto mag  = magc.real();
    return mag.sum();
}

Cela devrait essentiellement être plus proche de votre deuxième cas de test. Dans certains cas, j'ai eu de bonnes améliorations de performances tout en gardant la lisibilité (vous n'avez pas envie d'écrire l'expression de types de modèle). Bien sûr, votre kilométrage peut varier, donc de référence attentivement :)

0voto

DarioOO Points 784

Je veux juste que vous notez que vous ne le profilage de manière optimale, donc en fait le problème pourrait être votre méthode de profilage.

Puisqu'il y a beaucoup de choses, comme la localité de cache de prendre en compte que vous devez faire le profilage de cette façon:

int warmUpCycles = 100;
int profileCycles = 1000;

// TEST 1
for(int i=0; i<warmUpCycles ; i++)
      doTest1();

auto tick = std::chrono::steady_clock::now();
for(int i=0; i<profileCycles ; i++)
      doTest1();  
auto tock = std::chrono::steady_clock::now();
test1_us = (std::chrono::duration_cast<std::chrono::microseconds>(tock-tick)).count(); 

// TEST 2


// TEST 3

Une fois que vous avez fait le test de la bonne façon, alors vous pouvez venir à des conclusions..

Je soupçonne fortement que, puisque vous êtes le profilage d'une seule opération à la fois, on finit par l'aide de la version mise en cache sur le troisième essai, puisque les opérations sont susceptibles d'être re-commandé par le compilateur.

En outre, vous devriez essayer différents compilateurs pour voir si le problème est le déroulement de modèles (il y a une limite de profondeur à l'optimisation de modèles: il est probable que vous pouvez frapper avec une seule expression).

Aussi, si Propres à progresser sémantique, il n'y a aucune raison pourquoi une version devrait être plus rapide car il n'est pas toujours garanti que les expressions peuvent être optimisés.

Veuillez s'il vous plaît laissez-moi savoir, c'est intéressant. Assurez-vous également d'avoir permis à des optimisations avec des drapeaux comme -O3, le profilage sans optimisation est dénuée de sens.

Pour empêcher le compilateur d'optimiser tout de suite, utilisez la saisie initiale à partir d'un fichier ou d' cin , puis re-nourrir l'entrée dans les fonctions.

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