Nano Hash - криптовалюты, майнинг, программирование

Всегда ли стоит разделять задачу между потоками?

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

Недавно я выполнил простой тест. Я создал вектор данных и поровну разделил строки между потоками и сравнил время выполнения с одним рабочим потоком. Многопоточность оказалась победителем.

Вот мой код:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <numeric>
#include <chrono>

double g_sum = 0;
std::mutex g_mutex;

void worker(const std::vector<double>& vec)
{
    const auto vectorSum = std::accumulate(vec.begin(), vec.end(), 0.0);
    std::lock_guard<std::mutex> lg(g_mutex);
    std::cout << "Thread-Worker adding " << vectorSum << " to final sum ("<< g_sum <<")\n";
    g_sum += vectorSum;
}

int main()
{
    const int ROW_SIZE = 10000000;
    const int threadsSize = std::thread::hardware_concurrency();
    std::cout << "Task will be seprated on " << threadsSize << " threads\n";

    // data vector with row for every thread
    std::vector<std::vector<double>> dataVector;
    double fillVal = 1.1;
    for (auto i = 0; i < threadsSize; ++i, fillVal += 1.1)
    {
        dataVector.push_back(std::vector<double>(ROW_SIZE, fillVal));
    }

    std::vector<std::thread> threadContainer;
    auto start = std::chrono::system_clock::now();
    for (const auto& row : dataVector)
    {
        std::thread thread(&worker, std::ref(row));
        threadContainer.push_back(std::move(thread));
    }
    for (auto& thread : threadContainer)
    {
        thread.join();
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "threads time: " << elapsed_seconds.count() << "s\n";

    // main thread only
    g_sum = 0;
    start = std::chrono::system_clock::now();
    for (const auto& row : dataVector)
    {
        const auto vectorSum = std::accumulate(row.begin(), row.end(), 0.0);
        std::cout << "Main Thread adding " << vectorSum << " to final sum ("<< g_sum <<")\n";
        g_sum += vectorSum;
    }
    end = std::chrono::system_clock::now();
    elapsed_seconds = end-start;
    std::cout << "one-thread time: " << elapsed_seconds.count() << "s\n";
}

в wandbox (https://wandbox.org/permlink/qah5auBI3ZoAe7B2) с 3 логическими ядрами, многопоточность результаты по таймингу в два раза лучше, чем у однопоточного.

Мой тест правильный? Могу ли я предположить, что, пропуская дополнительное время на реализацию, всегда лучше разделить рабочую задачу между потоками?

09.02.2020

  • Нет, расщепление - не всегда правильный выбор. Создание потока стоит дорого. Нет серебряной пули - просто дайте своему приложению возможность разделить N потоков. Иногда вы могли использовать пул потоков. 09.02.2020
  • Создание потока имеет свою цену. Синхронизация между потоками имеет свою цену. Код для создания потоков и управления ими имеет свою стоимость. Потоки не являются == бесплатной производительностью. Они не всегда того стоят, а иногда могут даже замедлить работу. Вам нужна проблема, в которой они имеют смысл и где выгода перевешивает затраты. 09.02.2020
  • Попробуйте это с размером строки 10. Учет времени выполнения вывода в пределах установленного времени может исказить результаты. 10.02.2020
  • @MichaelChourdakis, что означает Thread-costly? Это только затраты времени компиляции? Пожалуйста объясните далее. 10.02.2020
  • @JesperJuhl Определенно потоки не являются бесплатными затратами, вот почему существует этот вопрос .... Знаете ли вы случайно, как можно сравнить стоимость управления потоками с прибылью от его использования ????? 10.02.2020
  • @ 1201ProgramAlarm, поэтому я создал хороший пример. Его очевидные потоки, как и любые другие абстракции, имеют свою стоимость .. Когда мы вычисляем сложность, у нас нет аргументов в пользу контейнеров одного размера, верно ???? 10.02.2020
  • Мой вопрос все еще актуален. Когда есть задача, которую можно разделить и выполнять одновременно, наша машина дает нам более одного логического ядра, а синхронизация минимальна - лучше использовать потоки или нет. 10.02.2020
  • @Piodo вы должны указать свое конкретное использование. 10.02.2020
  • Конкретное использование @MichaelChourdakis будет иметь конкретное решение. Я говорю о ситуации, когда у нас есть задача, которую можно разделить одновременно с минимальной стоимостью разделения, и у нас есть машина с несколькими логическими ядрами. Будет ли в этом случае наша компьютерная система использовать преимущества программы, разделенной пользователями между ядрами, или отдельная задача будет лучше оптимизирована компьютером. 10.02.2020
  • @Piodo нет ничего во время компиляции в потоках. Все проблемы зависят от ОС и производительности синхронизации во время выполнения и т. д. Нет минимальной разделенной стоимости, есть разделенная стоимость, которую несет ОС, а не язык. Оптимизаций нет вообще. Это нужно делать вручную. 10.02.2020

Ответы:


1

Параллелизм на основе задач с фиксированным числом потоков (без превышения лимита подписки) обычно является наиболее эффективным подходом. Однако задачи должны иметь разумный размер, чтобы избежать чрезмерных накладных расходов на планирование. IIRC, как показывает опыт для tbb, выполнение задачи должно требовать не менее 10 тыс. Циклов. Одна важная деталь, о которой вы должны быть осторожны, - это синхронизация между разными задачами. Поскольку обычно вы не знаете, в каком потоке выполняется задача, вы должны быть осторожны, чтобы не вызвать взаимоблокировки (например, порождая задачу, удерживая блокировку).

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

Небольшое примечание: я бы рекомендовал использовать существующую структуру планирования задач (например, tbb) вместо того, чтобы откатывать свою собственную.

10.02.2020
  • Кажется, это лучший ответ, о котором я хотел поговорить. В любом случае я собираюсь провести еще один тест по этой теме. Спасибо 11.02.2020
  • @Piodo, если вас вообще интересует тема параллелизма на основе задач, я рекомендую проверить Варианты планирования задач для систем с общей памятью. 11.02.2020

  • 2

    Нет однозначного ответа. Результаты многопоточности зависят от реализации. Это может быть быстрее или нет. Есть много тонких мест:

    1. Количество аппаратных потоков. Если вы создаете 100 потоков, когда количество аппаратных потоков равно 1, то многопоточное решение будет медленнее, чем однопоточное. Потому что время, затрачиваемое на переключение контекста потоков, будет слишком большим.
    2. Осуществление синхронизации. Например, ведение журнала - это медленная операция. Ведение журнала в контексте критического раздела - это плохо, потому что оно медленное. Вы можете ускорить это, если переместите логирование в другой поток: std::cout << "Thread-Worker adding " << vectorSum << " to final sum ("<< g_sum <<")\n";. Вы можете поместить в другой поток значения vectorSum и g_sum и зарегистрировать их. Это будет быстрее, чем ждать операции вывода в заблокированном разделе.
    10.02.2020
  • Я спросил об уже разделенных поровну отдельных задачах, которые могут работать одновременно. Он был разделен на разумное количество потоков (std :: hardware_concurrency). Синхронизация минимизирована. Вопрос до сих пор не получил ответа. 10.02.2020
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

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

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..