web-dev-qa-db-fra.com

Le moyen le plus rapide de nier un std :: vector

Supposons que j'ai un vecteur std :: de double, à savoir

std::vector<double> MyVec(N);

N est si important que les performances comptent. Supposons maintenant que MyVec est un vecteur non trivial (c'est-à-dire qu'il n'est pas un vecteur de zéros, mais a été modifié par une routine). Maintenant, j'ai besoin de la version niée du vecteur: j'ai besoin de -MyVec.

Jusqu'à présent, je l'ai mis en œuvre via

std::transform(MyVec.cbegin(),MyVec.cend(),MyVec.begin(),std::negate<double>());

Mais, vraiment, je ne sais pas si c'est quelque chose de sensé ou si c'est juste super naïf de mon côté.

Suis-je en train de le faire correctement? Ou std :: transform est juste une routine super lente dans ce cas?

PS: J'utilise tout le temps les bibliothèques BLAS et LAPACK, mais je n'ai rien trouvé qui corresponde à ce besoin particulier. Cependant, s'il existe une telle fonction dans BLAS/LAPACK qui est plus rapide que std :: transform, je serais heureux de le savoir.

26
enanone
#include <vector>
#include <algorithm>
#include <functional> 
void check()
{
    std::vector<double> MyVec(255);
    std::transform(MyVec.cbegin(),MyVec.cend(),MyVec.begin(),std::negate<double>());
}

Ce code sur https://godbolt.org/ avec l'option de copie -O3 génère Nice Assembly

.L3:
[...]
  cmp r8, 254
  je .L4
  movsd xmm0, QWORD PTR [rdi+2032]
  xorpd xmm0, XMMWORD PTR .LC0[rip]
  movsd QWORD PTR [rdi+2032], xmm0
.L4:

C'est difficile d'imaginer plus vite. Votre code est déjà parfait, n'essayez pas de déjouer le compilateur et utilisez du code C++ propre, il fonctionne presque à chaque fois.

28
ColdCat

Heureusement, les données de std::vector est contigu, vous pouvez donc multiplier par -1 en utilisant des intrinsèques vectorielles (en utilisant des charges/magasins non alignés et une gestion spéciale du débordement possible). Ou utiliser ippsMulC_64f/ippsMulC_64f_I à partir de la bibliothèque IPP d'Intel (vous aurez du mal à écrire quelque chose plus rapidement) qui utilisera les plus grands registres vectoriels disponibles pour votre plateforme: https://software.intel.com/en-us/ipp-dev- référence-mulc

Mise à jour: pour dissiper une certaine confusion dans les commentaires, la version complète d'Intel IPP est gratuite (bien que vous puissiez payer pour le support) et est disponible sur Linux, Windows et macOS.

17
keith

Comme d'autres l'ont mentionné, cela dépend complètement de votre cas d'utilisation. Le moyen le plus simple serait probablement quelque chose comme ceci:

 struct MyNegatingVect {
     MyVect data;
     bool negated = false;
     void negate() { negated = !negated; }
     // ... setter and getter need indirection ...
     // ..for example
     MyVect::data_type at(size_t index) { return negated ? - data.at(index) : data.at(index);
 };

Si cette indirection supplémentaire pour chaque accès vaut la peine de transformer la négation en définissant un seul bool dépend, comme déjà mentionné, de votre cas d'utilisation (en fait, je doute qu'il existe un cas d'utilisation où cela apporterait un avantage mesurable ).

Tout d'abord, une fonction générique negate pour les vecteurs de type arithmétique comme exemple:

#include <type_traits>
#include <vector>

...

template <typename arithmetic_type> std::vector<arithmetic_type> &
negate (std::vector<arithmetic_type> & v)
{
    static_assert(std::is_arithmetic<arithmetic_type>::value,
        "negate: not an arithmetic type vector");

    for (auto & vi : v) vi = - vi;

    // note: anticipate that a range-based for may be more amenable
    // to loop-unrolling, vectorization, etc., due to fewer compiler
    // template transforms, and contiguous memory / stride.

    // in theory, std::transform may generate the same code, despite
    // being less concise. very large vectors *may* possibly benefit
    // from C++17's 'std::execution::par_unseq' policy?

    return v;
}

Votre souhait d'un canonique unaire operator - la fonction va nécessiter la création d'un temporaire, sous la forme:

std::vector<double> operator - (const std::vector<double> & v)
{
    auto ret (v); return negate(ret);
}

Ou génériquement:

template <typename arithmetic_type> std::vector<arithmetic_type>
operator - (const std::vector<arithmetic_type> & v)
{
    auto ret (v); return negate(ret);
}

pas tentez d'implémenter l'opérateur comme:

template <typename arithmetic_type> std::vector<arithmetic_type> &
operator - (std::vector<arithmetic_type> & v)
{
    return negate(v);
}

Tandis que (- v) annulera les éléments et retournera le vecteur modifié sans avoir besoin d'un temporaire, il casse les conventions mathématiques en définissant efficacement: v = - v; Si tel est votre objectif, utilisez la fonction negate. Ne cassez pas l'évaluation attendue de l'opérateur!


clang, avec avx512 activé, génère cette boucle, annulant un impressionnant 64 doubles par itération - entre la gestion de la longueur avant/après:

        vpbroadcastq    LCPI0_0(%rip), %zmm0
        .p2align        4, 0x90
LBB0_21:
        vpxorq  -448(%rsi), %zmm0, %zmm1
        vpxorq  -384(%rsi), %zmm0, %zmm2
        vpxorq  -320(%rsi), %zmm0, %zmm3
        vpxorq  -256(%rsi), %zmm0, %zmm4
        vmovdqu64       %zmm1, -448(%rsi)
        vmovdqu64       %zmm2, -384(%rsi)
        vmovdqu64       %zmm3, -320(%rsi)
        vmovdqu64       %zmm4, -256(%rsi)
        vpxorq  -192(%rsi), %zmm0, %zmm1
        vpxorq  -128(%rsi), %zmm0, %zmm2
        vpxorq  -64(%rsi), %zmm0, %zmm3
        vpxorq  (%rsi), %zmm0, %zmm4
        vmovdqu64       %zmm1, -192(%rsi)
        vmovdqu64       %zmm2, -128(%rsi)
        vmovdqu64       %zmm3, -64(%rsi)
        vmovdqu64       %zmm4, (%rsi)
        addq    $512, %rsi              ## imm = 0x200
        addq    $-64, %rdx
        jne     LBB0_21

gcc-7.2.0 génère une boucle similaire, mais semble insister sur l'adressage indexé.

3
Brett Hale

Utilisez for_each

std::for_each(MyVec.begin(), MyVec.end(), [](double& val) { val = -val });

ou parallèle C++ 17

std::for_each(std::execution::par_unseq, MyVec.begin(), MyVec.end(), [](double& val) { val = -val });
0
Surt