86 votes

Comprendre std :: hardware_destructive_interference_size et std :: hardware_constructive_interference_size

C++17 ajoutés std::hardware_destructive_interference_size et std::hardware_constructive_interference_size. Tout d'abord, je pensais que c'est juste un portable de façon à obtenir la taille d'un cache L1 de la ligne, mais qui est une simplification exagérée.

Questions:

  • Comment sont ces constantes liées au cache L1 de la ligne de taille?
  • Est-il un bon exemple qui démontre leur cas d'utilisation?
  • Les deux sont définis static constexpr. N'est-ce pas un problème si vous créer un fichier binaire et de l'exécuter sur d'autres machines avec différentes tailles de ligne de cache? Comment peut-il protéger contre les faux partage dans ce scénario, lorsque vous n'êtes pas sur l'ordinateur de votre code sera exécuté?

77voto

GManNickG Points 155079

Le but de ces constantes est en effet pour obtenir le cache de la ligne de taille. Le meilleur endroit pour lire, au sujet de la justification pour eux, c'est la proposition elle-même:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0154r1.html

Je vais vous citer un extrait de la justification ici pour la facilité de lecture:

[...] la granularité de la mémoire qui n'interfère pas (au premier ordre) [est] communément appelé le cache de la ligne de taille.

Les utilisations de cache-ligne de taille se divisent en deux grandes catégories:

  • Éviter une interférence destructive (faux-sharing) entre les objets avec temporellement disjoints d'exécution des modèles d'accès de partir de différents threads.
  • La promotion de l'interférence constructive (vrai partage, entre les objets qui ont temporairement locales d'exécution des schémas d'accès.

La plupart des sigificant problème avec cette utile mise en œuvre de la quantité est la remise en question de la transférabilité des méthodes utilisées dans la pratique courante pour déterminer sa valeur, en dépit de leur fréquence et de la popularité en tant que groupe. [...]

Notre objectif est de contribuer à une modeste invention pour cette cause, d'abstractions pour cette quantité, qui peut être défini de façon conservatrice pour les fins de réalisation:

  • Interférence Destructive de taille: un nombre approprié comme un décalage entre les deux objets susceptibles d'éviter les fausses-pour le partage de runtime différents modèles d'accès de partir de différents threads.
  • L'interférence Constructive taille: un nombre qui convient à la limite de deux objets combiné empreinte mémoire de la taille et de la base de l'alignement sur le risque d'encourager le vrai partage entre eux.

Dans les deux cas, ces valeurs sont fournies sur une qualité de mise en œuvre, à titre purement conseils qui sont susceptibles d'améliorer les performances. Ces sont idéales portable valeurs à utiliser avec l' alignas() de mots clés, pour lesquelles il n'existe actuellement presque pas de standard pris en charge portable utilise.


"Comment sont ces constantes liées à la L1 taille de ligne de cache?"

En théorie, assez directement.

Supposons que le compilateur sait exactement ce que l'architecture, vous serez en cours d'exécution sur ces serait presque certainement vous donner le cache L1 de la ligne de taille justement. (Comme indiqué plus loin, c'est une grosse supposition.)

Pour ce que ça vaut, j'aurais presque toujours attendons à ce que ces valeurs soient les mêmes. Je crois que la seule raison pour laquelle ils sont déclarés séparément pour l'exhaustivité. (Cela dit, peut-être un compilateur veut estimation de cache L2 de la ligne de taille du cache L1 de la ligne de taille pour l'interférence constructive; je ne sais pas si ce serait réellement utile, cependant).


"Est-il un bon exemple qui démontre leur cas d'utilisation?"

Au bas de cette réponse j'ai attaché un long programme de test qui démontre faux de partage et de vrai partage.

Il démontre faux-partage par l'allocation d'un tableau de int wrappers: dans un cas, plusieurs éléments s'intègrent dans le cache L1-ligne, et dans l'autre un seul élément reprend le cache L1-ligne. Dans une boucle serrée, un seul, un élément fixe est choisi dans le tableau et mis à jour à plusieurs reprises.

Il démontre le véritable partage par l'allocation d'une seule paire d'entiers dans du papier d'emballage: dans un cas, les deux ints au sein de la paire ne rentrent pas dans le cache L1 de la ligne de la taille de l'ensemble, et dans l'autre, ils le font. Dans une boucle serrée, chaque élément de la paire est mis à jour à plusieurs reprises.

Notez que le code pour l'accès à l'objet sous test ne pas changer; la seule différence est la mise en page et l'alignement des objets eux-mêmes.

Je n'ai pas de C++17 compilateur (et d'assumer la plupart des gens actuellement ne sont pas non plus), j'ai donc remplacé les constantes en question avec mes propres. Vous avez besoin de mettre à jour ces valeurs pour être précis sur votre machine. Cela dit, 64 octets est probablement la valeur correcte sur moderne typique de matériel bureautique (au moment de la rédaction).

Avertissement: le test va utiliser tous les cœurs sur vos machines, et d'allouer ~256 mo de mémoire. N'oubliez pas de compiler avec les optimisations!

Sur ma machine, la sortie est:

Matériel de simultanéité: 16
sizeof(naive_int): 4
alignof(naive_int): 4
sizeof(cache_int): 64
alignof(cache_int): 64
sizeof(bad_pair): 72
alignof(bad_pair): 4
sizeof(good_pair): 8
alignof(good_pair): 4
L'exécution de naive_int test.
Durée moyenne: 0.0873625 secondes, inutile résultat: 3291773
L'exécution de cache_int test.
Durée moyenne: 0.024724 secondes, inutile résultat: 3286020
L'exécution de bad_pair test.
Durée moyenne: 0.308667 secondes, inutile résultat: 6396272
L'exécution de good_pair test.
Durée moyenne: 0.174936 secondes, inutile résultat: 6668457

Je reçois ~3,5 x speedup en évitant les faux-partage, et ~1,7 x speedup en assurant un vrai partage.


"Les deux sont définis statique constexpr. N'est-ce pas un problème si vous créer un fichier binaire et de l'exécuter sur d'autres machines avec différentes tailles de ligne de cache? Comment peut-il protéger contre les faux partage dans ce scénario, lorsque vous n'êtes pas sur l'ordinateur de votre code sera exécuté?"

Ce sera effectivement un problème. Ces constantes ne sont pas garanties à la carte pour tous les cache-ligne de taille sur la machine cible en particulier, mais sont destinés à être la meilleure approximation, le compilateur peut trouver.

Ceci est indiqué dans la proposition, et dans l'annexe, ils donnent un exemple de la façon dont certains des bibliothèques de tenter de détecter le cache de la ligne de taille au moment de la compilation basée sur les différentes astuces et des macros. Vous êtes garantis que cette valeur est au moins alignof(max_align_t), ce qui est à l'évidence une limite inférieure.

En d'autres termes, cette valeur doit être utilisée comme votre secours cas, vous êtes libre de définir une valeur précise si vous le connaissez, par exemple:

constexpr std::size_t cache_line_size() {
#ifdef KNOWN_L1_CACHE_LINE_SIZE
  return KNOWN_L1_CACHE_LINE_SIZE;
#else
  return std::hardware_destructive_interference_size;
#endif
}

Lors de la compilation, si vous voulez assumer un cache-ligne de taille il suffit de définir KNOWN_L1_CACHE_LINE_SIZE.

Espérons que cette aide!

Programme de test:

#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

// !!! YOU MUST UPDATE THIS TO BE ACCURATE !!!
constexpr std::size_t hardware_destructive_interference_size = 64;

// !!! YOU MUST UPDATE THIS TO BE ACCURATE !!!
constexpr std::size_t hardware_constructive_interference_size = 64;

constexpr unsigned kTimingTrialsToComputeAverage = 100;
constexpr unsigned kInnerLoopTrials = 1000000;

typedef unsigned useless_result_t;
typedef double elapsed_secs_t;

//////// CODE TO BE SAMPLED:

// wraps an int, default alignment allows false-sharing
struct naive_int {
    int value;
};
static_assert(alignof(naive_int) < hardware_destructive_interference_size, "");

// wraps an int, cache alignment prevents false-sharing
struct cache_int {
    alignas(hardware_destructive_interference_size) int value;
};
static_assert(alignof(cache_int) == hardware_destructive_interference_size, "");

// wraps a pair of int, purposefully pushes them too far apart for true-sharing
struct bad_pair {
    int first;
    char padding[hardware_constructive_interference_size];
    int second;
};
static_assert(sizeof(bad_pair) > hardware_constructive_interference_size, "");

// wraps a pair of int, ensures they fit nicely together for true-sharing
struct good_pair {
    int first;
    int second;
};
static_assert(sizeof(good_pair) <= hardware_constructive_interference_size, "");

// accesses a specific array element many times
template <typename T, typename Latch>
useless_result_t sample_array_threadfunc(
    Latch& latch,
    unsigned thread_index,
    T& vec) {
    // prepare for computation
    std::random_device rd;
    std::mt19937 mt{ rd() };
    std::uniform_int_distribution<int> dist{ 0, 4096 };

    auto& element = vec[vec.size() / 2 + thread_index];

    latch.count_down_and_wait();

    // compute
    for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) {
        element.value = dist(mt);
    }

    return static_cast<useless_result_t>(element.value);
}

// accesses a pair's elements many times
template <typename T, typename Latch>
useless_result_t sample_pair_threadfunc(
    Latch& latch,
    unsigned thread_index,
    T& pair) {
    // prepare for computation
    std::random_device rd;
    std::mt19937 mt{ rd() };
    std::uniform_int_distribution<int> dist{ 0, 4096 };

    latch.count_down_and_wait();

    // compute
    for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) {
        pair.first = dist(mt);
        pair.second = dist(mt);
    }

    return static_cast<useless_result_t>(pair.first) +
        static_cast<useless_result_t>(pair.second);
}

//////// UTILITIES:

// utility: allow threads to wait until everyone is ready
class threadlatch {
public:
    explicit threadlatch(const std::size_t count) :
        count_{ count }
    {}

    void count_down_and_wait() {
        std::unique_lock<std::mutex> lock{ mutex_ };
        if (--count_ == 0) {
            cv_.notify_all();
        }
        else {
            cv_.wait(lock, [&] { return count_ == 0; });
        }
    }

private:
    std::mutex mutex_;
    std::condition_variable cv_;
    std::size_t count_;
};

// utility: runs a given function in N threads
std::tuple<useless_result_t, elapsed_secs_t> run_threads(
    const std::function<useless_result_t(threadlatch&, unsigned)>& func,
    const unsigned num_threads) {
    threadlatch latch{ num_threads + 1 };

    std::vector<std::future<useless_result_t>> futures;
    std::vector<std::thread> threads;
    for (unsigned thread_index = 0; thread_index != num_threads; ++thread_index) {
        std::packaged_task<useless_result_t()> task{
            std::bind(func, std::ref(latch), thread_index)
        };

        futures.push_back(task.get_future());
        threads.push_back(std::thread(std::move(task)));
    }

    const auto starttime = std::chrono::high_resolution_clock::now();

    latch.count_down_and_wait();
    for (auto& thread : threads) {
        thread.join();
    }

    const auto endtime = std::chrono::high_resolution_clock::now();
    const auto elapsed = std::chrono::duration_cast<
        std::chrono::duration<double>>(
            endtime - starttime
            ).count();

    useless_result_t result = 0;
    for (auto& future : futures) {
        result += future.get();
    }

    return std::make_tuple(result, elapsed);
}

// utility: sample the time it takes to run func on N threads
void run_tests(
    const std::function<useless_result_t(threadlatch&, unsigned)>& func,
    const unsigned num_threads) {
    useless_result_t final_result = 0;
    double avgtime = 0.0;
    for (unsigned trial = 0; trial != kTimingTrialsToComputeAverage; ++trial) {
        const auto result_and_elapsed = run_threads(func, num_threads);
        const auto result = std::get<useless_result_t>(result_and_elapsed);
        const auto elapsed = std::get<elapsed_secs_t>(result_and_elapsed);

        final_result += result;
        avgtime = (avgtime * trial + elapsed) / (trial + 1);
    }

    std::cout
        << "Average time: " << avgtime
        << " seconds, useless result: " << final_result
        << std::endl;
}

int main() {
    const auto cores = std::thread::hardware_concurrency();
    std::cout << "Hardware concurrency: " << cores << std::endl;

    std::cout << "sizeof(naive_int): " << sizeof(naive_int) << std::endl;
    std::cout << "alignof(naive_int): " << alignof(naive_int) << std::endl;
    std::cout << "sizeof(cache_int): " << sizeof(cache_int) << std::endl;
    std::cout << "alignof(cache_int): " << alignof(cache_int) << std::endl;
    std::cout << "sizeof(bad_pair): " << sizeof(bad_pair) << std::endl;
    std::cout << "alignof(bad_pair): " << alignof(bad_pair) << std::endl;
    std::cout << "sizeof(good_pair): " << sizeof(good_pair) << std::endl;
    std::cout << "alignof(good_pair): " << alignof(good_pair) << std::endl;

    {
        std::cout << "Running naive_int test." << std::endl;

        std::vector<naive_int> vec;
        vec.resize((1u << 28) / sizeof(naive_int));  // allocate 256 mibibytes

        run_tests([&](threadlatch& latch, unsigned thread_index) {
            return sample_array_threadfunc(latch, thread_index, vec);
        }, cores);
    }
    {
        std::cout << "Running cache_int test." << std::endl;

        std::vector<cache_int> vec;
        vec.resize((1u << 28) / sizeof(cache_int));  // allocate 256 mibibytes

        run_tests([&](threadlatch& latch, unsigned thread_index) {
            return sample_array_threadfunc(latch, thread_index, vec);
        }, cores);
    }
    {
        std::cout << "Running bad_pair test." << std::endl;

        bad_pair p;

        run_tests([&](threadlatch& latch, unsigned thread_index) {
            return sample_pair_threadfunc(latch, thread_index, p);
        }, cores);
    }
    {
        std::cout << "Running good_pair test." << std::endl;

        good_pair p;

        run_tests([&](threadlatch& latch, unsigned thread_index) {
            return sample_pair_threadfunc(latch, thread_index, p);
        }, cores);
    }
}

21voto

Murat Karakuş Points 130

J'aurais presque toujours attendons à ce que ces valeurs soient les mêmes.

Concernant ci-dessus, je tiens à apporter une contribution mineure à la accepté de répondre. Il y a un moment, j'ai vu un très bon cas d'utilisation où ces deux devraient être définie séparément dans l' folly bibliothèque. Veuillez voir la mise en garde à propos de Intel processeur Sandy Bridge.

https://github.com/facebook/folly/blob/3af92dbe6849c4892a1fe1f9366306a2f5cbe6a0/folly/lang/Align.h

//  Memory locations within the same cache line are subject to destructive
//  interference, also known as false sharing, which is when concurrent
//  accesses to these different memory locations from different cores, where at
//  least one of the concurrent accesses is or involves a store operation,
//  induce contention and harm performance.
//
//  Microbenchmarks indicate that pairs of cache lines also see destructive
//  interference under heavy use of atomic operations, as observed for atomic
//  increment on Sandy Bridge.
//
//  We assume a cache line size of 64, so we use a cache line pair size of 128
//  to avoid destructive interference.
//
//  mimic: std::hardware_destructive_interference_size, C++17
constexpr std::size_t hardware_destructive_interference_size =
    kIsArchArm ? 64 : 128;
static_assert(hardware_destructive_interference_size >= max_align_v, "math?");

//  Memory locations within the same cache line are subject to constructive
//  interference, also known as true sharing, which is when accesses to some
//  memory locations induce all memory locations within the same cache line to
//  be cached, benefiting subsequent accesses to different memory locations
//  within the same cache line and heping performance.
//
//  mimic: std::hardware_constructive_interference_size, C++17
constexpr std::size_t hardware_constructive_interference_size = 64;
static_assert(hardware_constructive_interference_size >= max_align_v, "math?");

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