增加线程数,但程序不能更快地运行 C++ OpenMP 选择排序

Posted

技术标签:

【中文标题】增加线程数,但程序不能更快地运行 C++ OpenMP 选择排序【英文标题】:Increasing threads count, but program doesn't work faster C++ OpenMP Selection Sort 【发布时间】:2018-11-14 21:43:05 【问题描述】:

我正在尝试使用 C++ OpenMP 编写选择排序算法。我写了代码,它排序了,但是随着线程数的增加,SelectionSort 算法的运行时间也增加了......我刚开始使用 OpenMP,所以我不太擅长:D 我不能使用 OpenMP reduction,因为我正在使用 VisualStudio Community 2017 IDE .. 感谢您的帮助 ;)

#include "pch.h"
#include <iostream>
#include <omp.h>
#include <random>
#include <ctime>
#include <iomanip>
#include <algorithm>

using namespace std;

const int n = 10000;                            // count of numbers in vector

// ------------------- FUNCTIONS HEADERS -----------------------
vector<int> FillVector();
vector<int> SelectionSort(vector<int> data, int num_th);
void PrintArray(vector<int> data);

int main()

    std::vector<int> test_1(n);
    std::vector<int> test_2(n);
    std::vector<int> test_3(n);
    std::vector<int> test_4(n);

    cout << test_1.size() << endl;          // size of vector

    test_1 = FillVector();

    std::copy(std::begin(test_1), std::end(test_1), std::begin(test_2));    // copy vector test_1 to test_2
    std::copy(std::begin(test_1), std::end(test_1), std::begin(test_3));    // copy vector test_1 to test_3
    std::copy(std::begin(test_1), std::end(test_1), std::begin(test_4));    // copy vector test_1 to test_4

    // Testing times of sorting with different threads count


    // Number of Threads: 2
    int num_th = 2;
    clock_t begin = clock();
    test_1 = SelectionSort(test_1, num_th);     // sort vector test_1
    clock_t end = clock();
    double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
    cout << elapsed_secs << endl;

    // Number of Threads: 4
    num_th = 4;
    begin = clock();
    test_2 = SelectionSort(test_2, num_th);     // sort vector test_2
    end = clock();
    elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
    cout << elapsed_secs << endl;

    // Number of Threads: 8
    num_th = 8;
    begin = clock();
    test_3 = SelectionSort(test_3, num_th);     // sort vector test_3
    end = clock();
    elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
    cout << elapsed_secs << endl;

    // Number of Threads: 16
    num_th = 16;
    begin = clock();
    test_4 = SelectionSort(test_4, num_th);     // sort vector test_4
    end = clock();
    elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
    cout << elapsed_secs << endl;

    return 0;


// ----------------- METHODS --------------------------------

// Fill vector with random integers
vector<int> FillVector() 
    vector<int> temp(n);

    srand(time(NULL));
    for (int i = 0; i < n; i++)
        temp[i] = rand() % n + 1;

    return temp;


// EXECUTE parallel Selection Sort usin OpenMP
vector<int> SelectionSort(vector<int> data, int num_th)

    // start parallel method (OpenMP)
    //VEIKIA !!!!!!
    vector<int> temp(n);
    std::copy(std::begin(data), std::end(data), std::begin(temp));      // make a temporary copy of array

    omp_set_num_threads(num_th);                                        // set threads number
    for (int i = 0; i < n - 1; i++)
    
        int min_index = i;
#pragma omp parallel                                                    // start OpenMP parallel code block
        for (int j = i + 1; j < n; j++)
            if (temp[j] < temp[min_index])
                min_index = j;
        std::swap(temp[i], temp[min_index]);                            // std swap method
    

    return temp;


// PRINT vector as 2D array
void PrintArray(vector<int> data)

    int rows, elem = 20;                                                // declare rows variable and column count

    if (n % elem != 0)                                                  // calculate rows count
        rows = n / elem + 1;
    else
        rows = n / elem;

    int iii = 0;
    for (int i = 0; i < rows; i++) 
        for (int j = 0; j < elem; j++) 
            if (iii != n) 
                cout << setw(3) << left << data[iii] << " ";
                iii++;
            
            else
                break;
        
        cout << endl;
    
    cout << endl;

结果(当 n = 5000 时):

尺寸:5000 线程 2:5.607 线程 4:8.421 线程 8:10.979 线程 16:27.989

OPEN image

【问题讨论】:

您通常希望在 omp parallel 中使用 reduction 子句,但这不适用于查找最小元素的索引。 我使用 OMP 已经有一段时间了,但我很确定你应该使用 #pragma omp parallel for,而不是 #pragma omp parallel。而且,您在内部循环上执行此操作的事实意味着过多的线程连接。更不用说min_index 的固有问题了。 【参考方案1】:

首先,考虑选择排序算法。它所做的是重复扫描(子)数组以找到最小的元素,然后将其移到前面。这个过程基本上是迭代的,即。在从 1 扫描到 n 之前,您必须从 0 扫描到 n。在您的情况下,即使您有多个线程,您仍然必须按顺序从开始位置线性扫描到结束位置,因此您的额外线程无济于事。

由于该算法不可并行化,因此添加额外的线程无济于事。事实上,它会减慢进程,因为必须初始化新线程。

【讨论】:

【参考方案2】:

选择排序确实很难并行化,但您认为可以并行化需要在数组其余部分中找到最小元素的部分是正确的。

#pragma omp parallel for (int j = i + 1; j < n; j++) if (temp[j] < temp[min_index]) min_index = j; 我看到的第一个问题是您使用普通的 omp 并行。此命令为每个可用线程(对于第 num_th 个线程)启动同一段代码。这绝对不会给您带来任何速度改进,并且可能导致 min_index 可以包含一个不是最小的值索引,因为多个线程可以同时访问同一个变量,这意味着他们不知道另一个线程是否会覆盖变量紧随其后的索引值是否较小。

您可以使用#pragma omp parallel for 修复此问题,它将区间 [i+1, n) 划分为 num_thr 部分。但是对变量的并发访问有点棘手。在这种情况下,最好的解决方案是使用 omp reduction 但就像你说的那样你不能使用它。然后还有其他选项,例如使用atomic variable、critical section(绝对不会帮助您)或compare and swap method,但我怀疑这些是否会给您的算法带来任何显着的速度提升。

我建议您使用一些更“并行化友好”的排序算法,例如合并排序或快速排序。

【讨论】:

【参考方案3】:

扩展@Ramade 的答案: 有问题的代码是:

    for (int i = 0; i < n - 1; i++)
    
        int min_index = i;
#pragma omp parallel                                                    // start OpenMP parallel code block
        for (int j = i + 1; j < n; j++)
            if (temp[j] < temp[min_index])
                min_index = j;
        std::swap(temp[i], temp[min_index]);                            // std swap method
    

你需要的是:

#pragma omp for 实际分配内部循环

一些减少方案以避免每个线程访问共享变量 [min_index]

最好避免在最外层循环的每次迭代中产生新线程,以减少 openmp 开销

例如:

omp_set_num_threads(num_th);                                        // set threads number

// Spawn all threads once
int min_index_all=0;
int min_value_all=INT_MAX;
#pragma omp parallel shared(temp,min_index_all,min_value_all)

    //these are private variables
    int min_index,min_value;

    for (int i = 0; i < n - 1; i++)
        min_index = i;
        min_value = temp[min_index];

        // OpenMP distributed loop
        // nowait because once the loop is complete the thread can do the reduction part immediately
        // static schedule because we don't expect a large unbalance of workload between threads
        #pragma omp parallel schedule(static) nowait
        for (int j = i + 1; j < n; j++)
            if (temp[j] < temp[min_index])
                min_index = j;
                min_value = temp[min_index];
            

        // reduction part in a critical section
        #pragma omp critical
        
            if (min_value<min_value_all)
                min_index_all = min_index;
                min_value_all = min_value;
            
        

        // explicit barrier: wait for all threads to have finished the reduction
        #pragma omp barrier

        // swap done by a single thread
        // + initialization of shared variables for next iteration
        #pragma omp single
        
            std::swap(temp[i], temp[min_index]);                            // std swap method
            min_index_all = i+1;
            min_value_all = INT_MAX;
        

        // implicit barrier after omp single
        // all threads will wait there until the single section is done
        // then they will move together to next iteration of the outer loop
    // end for i loop

不过,您尝试并行化的循环非常简单,并且可能在单个线程上非常有效地运行,直到 n 的大值(对于这种情况,选择排序不是一种有效的算法)。 OpenMP 并行化会带来一些开销,并且如果内部循环的迭代次数很少,则会成为一种负担。

您可能需要拆分外循环,以便在内循环有多次迭代时使用并行代码,如果迭代次数很少,则使用串行代码。

#define SOME_VALUE 20000 // To be tweaked 
int i;
for (i = 0; i < n - 1 - SOME_VALUE; i++)
  //parallel version of the code


i = (n - 1 - SOME_VALUE < 0) ? 0 : n-1-SOME_VALUE;
for (; i < n - 1; i++)
  // finish with a serial version of the code

【讨论】:

以上是关于增加线程数,但程序不能更快地运行 C++ OpenMP 选择排序的主要内容,如果未能解决你的问题,请参考以下文章

Java线程项目奇怪的行为,同时增加线程数

面试官:单核 CPU 支持 Java 多线程吗?什么?

如何增加linux中运行cppcheck进程的线程数

请问JMeter能不能比较两次测试运行的结果,是否不能测试JavaScript?

增加 C++ 程序 CPU 使用率

线程总是在增加