To naprawdę powinien być komentarz w odpowiedzi na odpowiedź Matsa Peterssona, ale chciałem podać przykłady kodu.
Problem stanowi rywalizacja konkretnego zasobu, a także pamięci podręcznej.
Alternatywa 1:
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([&k]() { // capture k by reference so we all use the same k.
while (k < ITERATIONS) {
k++;
}
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Tutaj gwinty utrzymują dla pojedynczej zmiennej, występując zarówno odczytu i zapisu, który zmusza go do ping-ponga, powodując rywalizacji i co jednolitym gwintowanych przypadku najbardziej efektywny.
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::atomic<uint64_t> k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([&]() {
// Imperfect division of labor, we'll fall short in some cases.
for (size_t i = 0; i < ITERATIONS/numThreads; ++i) {
k++;
}
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Tutaj dzielimy pracy deterministycznie (upadamy afoul przypadkach numThreads nie jest dzielnikiem liczby iteracji, ale jest to na tyle blisko tej demonstracji). Niestety wciąż napotykamy na rywalizację o dostęp do współużytkowanego elementu w pamięci.
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::vector<uint64_t> ks;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([=, &ks]() {
auto& k = ks[t];
// Imperfect division of labor, we'll fall short in some cases.
for (size_t i = 0; i < ITERATIONS/numThreads; ++i) {
k++;
}
});
}
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
k += ks[t];
}
return 0;
}
Znowu jest deterministyczny o podziale obciążeń, i spędzamy niewielką ilość wysiłku w końcu do sortowania wyników. Jednak nie zrobiliśmy nic, aby zapewnić dystrybucję liczników faworyzujących zdrową dystrybucję procesorów. W tym celu:
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
static const uint64_t ITERATIONS = 10000000000ULL;
#define CACHE_LINE_SIZE 128
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::mutex kMutex;
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([=, &k]() {
alignas(CACHE_LINE_SIZE) uint64_t myK = 0;
// Imperfect division of labor, we'll fall short in some cases.
for (uint64_t i = 0; i < ITERATIONS/numThreads; ++i) {
myK++;
}
kMutex.lock();
k += myK;
kMutex.unlock();
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Tu uniknąć rywalizacji pomiędzy nićmi w dół do poziomu linii pamięci podręcznej, z wyjątkiem jednego przypadku na końcu, gdzie używamy mutex kontrolować synchronizację. W przypadku tego trywialnego obciążenia muteks będzie miał jeden względny koszt. Alternatywnie możesz użyć alignas, aby zapewnić każdemu wątkowi własną pamięć w zewnętrznym zasięgu i podsumować wyniki po złączeniach, eliminując potrzebę stosowania muteksu. Zostawiam to jako ćwiczenie dla czytelnika.
Wszystkie wątki robią jest walnąć głowy próby odczytu i zapisu do k. A kiedy pierwszy się kończy, musi jeszcze poczekać na sekundę. – chris
@chris: 'k' nie jest' volatile', więc wątki nie konkurują ze sobą, ponieważ skutecznie nie dzielą 'k'. –
@MooingDuck: 'volatile' zapewnia zapisywanie w pamięci, ale brak' volatile' nie zapobiega im. Pytanie brzmi: "... bez optymalizacji ..." i jest typowe dla niezoptymalizowanych kompilacji, aby bardzo dokładnie przestrzegać instrukcji programu, a nie "cache" w rejestrach. Na najnowszym sprzęcie firmy Intel/AMD następuje automatyczne przepłukanie między pamięciami podręcznymi, które uzyskują dostęp do tego samego adresu pamięci, który spowolniłby to działanie. –