Два почти одинаковых многопоточных кода имеют очень разное время выполнения

у меня странная проблема с многопоточными программами на C++. я написал первый код и не увидел никакого улучшения производительности по сравнению с однопоточным кодом. Я продолжал экспериментировать со смещением переменных в функциях, и второй код занял почти половину времени первого кода.

Первый код:

#include<iostream>
#include<thread>
#include<chrono>

typedef unsigned long long ull;
ull evenSum = 0;
ull oddSum = 0;
void findEvenSum(ull start, ull end){
    
    for(ull i = start; i<=end; i++){
        if ((i&1) == 0){
            evenSum+=i;
        }
    }
    std::cout<<"Even Sum: "<<evenSum<<"\n";
}

void findOddSum(ull start, ull end){
    
    for(ull i = start; i<=end; i++){
        if ((i&1) == 1){
            oddSum+=i;
        }
    }
    std::cout<<"Odd Sum: "<<oddSum<<"\n";
}
int main(){
    ull start = 0, end = 9999999999;
    auto startTime = std::chrono::high_resolution_clock::now();

    std::thread evenSumCalculator(findEvenSum, start, end);
    std::thread oddSumCalculator(findOddSum, start, end);

    evenSumCalculator.join();
    oddSumCalculator.join();

    // findEvenSum(start, end);
    // findOddSum(start, end);

    auto stopTime = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stopTime - startTime);
    std::cout<<"time taken: "<<duration.count()<<" microseconds\n";

    return 0;
}

Второй код:

#include<iostream>
#include<thread>
#include<chrono>

typedef unsigned long long ull;
ull eSum = 0;
ull oSum = 0;

void findEvenSum(ull start, ull end){
    ull evenSum = eSum;
    for(ull i = start; i<=end; i++){
        if ((i&1) == 0){
            evenSum+=i;
        }
    }
    std::cout<<"Even Sum: "<<evenSum<<"\n";
}

void findOddSum(ull start, ull end){
    ull oddSum = oSum;
    for(ull i = start; i<=end; i++){
        if ((i&1) == 1){
            oddSum+=i;
        }
    }
    std::cout<<"Odd Sum: "<<oddSum<<"\n";
}
int main(){
    ull start = 0, end = 9999999999;
    auto startTime = std::chrono::high_resolution_clock::now();

    std::thread evenSumCalculator(findEvenSum, start, end);
    std::thread oddSumCalculator(findOddSum, start, end);

    evenSumCalculator.join();
    oddSumCalculator.join();

    // findEvenSum(start, end);
    // findOddSum(start, end);

    auto stopTime = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stopTime - startTime);
    std::cout<<"time taken: "<<duration.count()<<" microseconds\n";

    return 0;
}

вторая программа в два раза быстрее первой. я запускал программы без оптимизации

В чем причина такой разницы во времени работы?

И сколько времени длились обе казни?

Yksisarvinen 15.07.2024 16:45

я запускал программы без оптимизации" Почему? Кажется, вас беспокоит время выполнения программ. Зачем говорить компилятору, что это не так?

Drew Dormann 15.07.2024 16:46

В первом тесте evenSum и oddSum, вероятно, будут использовать одну и ту же строку кэша. Во втором тесте это неверно, поскольку они находятся в отдельных стопках. Google для «ложного обмена кэшем»

Richard Critten 15.07.2024 16:47

Вы также используете неправильные часы для отсчета времени. Вам следует использовать std::chrono::steady_clock.

Drew Dormann 15.07.2024 16:47

@DrewDormann, какое время мне следует использовать?

programmer of c 15.07.2024 16:48

Проблема вызвана так называемым «ложным разделением». По сути, проблема в том, что вы используете глобальные переменные. Эти аккумуляторы: evenSum и oddSum находятся в одной строке кэша, и процессор должен выполнить некоторую синхронизацию.

Marek R 15.07.2024 16:48

Какие меры мне следует предпринять, чтобы избежать таких проблем, как ложное совместное использование в многопоточных программах?

programmer of c 15.07.2024 16:50

Обычно старайтесь избегать глобальных переменных в многопоточном коде — это также упрощает рассуждения о безопасности потоков/гонках данных. Для задач, подобных описанным выше, используйте локальную переменную стека и обновляйте глобальную только один раз в конце потока.

Richard Critten 15.07.2024 16:52

Мне очень хочется закрыть это как дубликат `Что такое «ложный обмен»? Как воспроизвести/избежать этого?

Drew Dormann 15.07.2024 17:22

«Я запускал программы без оптимизации» Как правило, не делайте этого. В этом конкретном случае ваши результаты, вероятно, действительны, но это становится очевидным только постфактум, когда вы знаете, что вызывает разницу.

Jerry Coffin 15.07.2024 17:42

Кстати, две программы делают разные вещи, поскольку глобальные значения в конце не совпадают.

Jarod42 16.07.2024 12:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
11
119
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В первом случае у вас есть две переменные, расположенные в памяти близко друг к другу. Затем, если один поток хочет обновить, например, EvenSum, он запрашивает из кэша памяти строку с этой переменной в монопольном доступе (одно из состояний доступа протокола MESI). Существует высокая вероятность наличия нечетной суммы в этой строке кэша, поэтому второй поток теряет монопольный доступ к нечетной сумме и должен запрашивать ее снова, когда он ее обновит. Это называется ложный обмен, об этом можно прочитать здесь и поиграться с протоколом MESI здесь

Другие вопросы по теме