【问题标题】:Parallel on C++ isn't fast like C#C++ 上的并行并不像 C# 那样快
【发布时间】:2019-04-23 03:51:59
【问题描述】:

我在 C#

上有这样的代码
private static Random random = new Random();
public static string RandomString(int length)
{
    const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    return new string(Enumerable.Repeat(chars, length)
      .Select(s => s[random.Next(s.Length)]).ToArray());
}
     Task.Factory.StartNew(() => {       
                 System.Threading.Tasks.Parallel.For(0L, 10000000, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 10 }, n =>
                 {
                 Console.WirteLine(RandomString(12));
                 });
                }

为其添加并行方法,它可以在不到 8 秒的时间内运行生成 1000 万个随机字符串并使用所有 CPU 功率

我尝试在 C++

中再次执行此操作
string NonRepeatChar(int max_len)
{
    std::string valid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    std::random_device rd;
    std::mt19937 g(rd());
    std::shuffle(valid_chars.begin(), valid_chars.end(), g);
    std::string rand_str(valid_chars.begin(), valid_chars.begin() + max_len);
    return rand_str;
}

将代码应用到一些推荐的C++并行方法

void multiply()
{
for (size_t i = 0; i < 10; i++)
{
    for (size_t j = 0; j < 10; j++)
    {           
        for (int k = 0; k < 10; k++)
        {
            printf("%s\n",NonRepeatChar(10));
        }           
    }
}
}

class Foo {
public:
    Foo() : counter_(0) {}

    std::pair<string, std::future<void>> a_function(std::future<void>& f) {
        // Ensure that the background task from the previous iteration
        // has completed
        f.wait();

        // Set the task for the next iteration
        std::future<void> fut = std::async(std::launch::async, &Foo::background_task, this);

        // Do some work
        string value = NonRepeatChar(12);

        // Return the result and the future for the next iteration
        return std::make_pair(value.c_str(), std::move(fut));
    }

    void background_task() {
        ++counter_;
    }

private:
    std::atomic<int> counter_;
};

记录运行时间

int main()
{   
    clock_t tStart = clock();
    std::future<void> bleak = std::async(std::launch::deferred, []() {});

    Foo foo;

    for (size_t i = 0; i < 10000; ++i) {
        // Call the function
        std::pair<string, std::future<void>> result = foo.a_function(bleak);    
        bleak = std::move(result.second);   
        std::cout << result.first << "\n";
    }
    printf("Time taken: %.2fs\n", (double)(clock() - tStart) / CLOCKS_PER_SEC);
    return 0;
    }

这是我的结果:

10.98s//正常循环

8.76s//乘法

8.88s//Foo

显然该代码与原始循环相比没有任何区别,仅生成 10000 行,甚至没有像 C# 那样使用所有 CPU 能力。并行方法有问题吗?我该如何优化它?

【问题讨论】:

  • 您使用了哪些 C++ 编译器标志来编译您的程序?如果您正在运行未优化或“调试”构建,那么您向我们展示的有关时序信息的信息毫无意义。
  • Visual Studio,与 C# 相同。我使用的所有方法都不涉及外部库,如果这就是您的要求?
  • 不,我要问的是您是否正在运行 C++ 代码的“调试”版本。如果是这样,那么将您的配置更改为“发布”并重建您的代码。一般来说,任何涉及 C++ 代码的速度或慢的问题都必须附带用于构建测试代码的编译器选项。
  • 我只是将模式更改为“发布”,性能保持不变
  • 好。关键是我们有太多的问题不得不关闭、删除或放弃,因为原始发布者没有运行发布版本,然后发现发布版本快得多,使得问题变得毫无意义。基准必须始终伴随着用于构建程序的选项。

标签: c++ performance parallel-processing parallel.foreach


【解决方案1】:

您的 C++ 代码与您的 C# 代码完全不同。

在 C# 方面

您正在使用来自System.Threading.Tasks 命名空间的Parallel.For。这是一个高级构造,它允许循环的迭代并行运行,而不必以低级方式控制线程,因为任务会自动为您创建并以最适合您的方式调度到您的处理器内核你的系统。

对于您的特定代码,Parallel.For 配置为允许一次最多安排Environment.ProcessorCount * 10 工作线程。虽然不能保证(库的调度程序将有最后决定权),但这应该确保您的处理核心最佳地用于提交的工作负载,因为有足够的任务来占用所有核心,并且有足够大的工作队列来确保核心不会因为没有提前安排好的工作而挨饿。

在 C++ 方面

您正在使用asyncfuture,它们是允许您运行后台任务的较低级别的构造,但是您通过在每次迭代中强制同步来人为地限制并行度:

// Ensure that the background task from the previous iteration
// has completed
f.wait();

在 C++ 中实现与 C# 代码类似的行为的最简单(但不可移植)方法是使用 Microsoft 的 Parallel Patterns Library。这提供了与System.Threading.Tasks.Parallel.For 在C# 端提供的功能非常相似的功能,即concurrency::parallel_for

【讨论】:

  • 谢谢,我试试看
  • 好吧,我尝试按照您的建议使用 parallel_for,它确实运行得更快。使用我当前的代码 'parallel_for(0, 10000, [&](int n) {//my function} '。你有什么建议可以进一步改进它吗?在 C# 中,我设法计算并使用了所有线程Environment.ProcessorCount,我们可以用 'concurrency::parallel_for' 做同样的事情吗?
  • 至少有两种方法可以调整 PPL 的 parallel_for 的性能:通过将特定的分区器 (docs.microsoft.com/en-us/cpp/parallel/concrt/…) 传递给 parallel_for,以及使用 concurrency::Scheduler 类来调整用于安排工作。
【解决方案2】:

这是一个简单的单线程示例,说明可以使用混合 C/C++ 完成的工作 方法。游戏开发者使用混合“正式”C++ 代码的方法 而且看起来不像python。当然,就像 Marmite 一样,您喜欢或讨厌它,但无论如何,结果不言自明。

如果您想了解更多信息,我深表歉意。

这个特定示例在我的旧 AMD 机器上的单个线程上在 3.682 秒内生成 10M 字符串。 您可以启动少量异步工作程序 (

要更快,您需要手动展开循环,使用 SIMD 算法等。例如,这种情况适用于 SIMD 置换向量。

#include <stdint.h>
#include <stdio.h>

// This is typical of the random number generators used in professional games.
// It is less "correct" than mersenne twisters, for example, but much faster.
inline uint32_t fast_rand(int32_t &seed, uint32_t limit) {
  // Prevent infinite loops.
  //if (limit == 0) return 0;

  // Make a mask that has all 1s in the bottom few bits.
  // This reduces the number of iterations of the loop to ~1
  int leading_zeros = __builtin_clz(limit);
  int mask = 0xffffffff >> leading_zeros;

  // Loop until our result is in range using rotate and xor.
  do {
    seed = (seed << 1) ^ ((seed >> 31) & 0xa53a9be9);
  } while ((seed & mask) >= limit);

  return seed & mask;
}

int main() {
  // I'm using two seeds to prevent coupling.
  // On their own, their quantiles are pretty flat, but
  // in this example they couple, causing conditioning in the results.
  int32_t length_seed = (int32_t)0x95abcfad;
  int32_t swap_seed = (int32_t)0xba7235fab;

  for (int i = 0; i != 10000000; ++i) {
    // Note we don't use a std::string. These are very slow.
    char chars[] = 
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    auto num_chars = sizeof(chars) - 1;
    auto length = fast_rand(length_seed, num_chars-1) + 1;

    // Trim the string to the right length.
    chars[length] = 0;

    // Shuffle the characters.
    for (int j = 0; j != length; ++j) {
      int swapper = j + fast_rand(swap_seed, length - j);
      auto tmp = chars[j];
      chars[j] = chars[swapper];
      chars[swapper] = tmp;
    }

    // Print with puts (not iostreams).
    puts(chars);
  }
}

对于像这样的“热循环”示例,您应该在 Godbolt 或类似设备上检查您的结果。

clang 与 -O3 -mlzcnt 给出以下内部循环。

.LBB0_4:                                #   Parent Loop BB0_1 Depth=1
    mov     rsi, rax
    sub     rsi, rdx
    lzcnt   ecx, esi
    mov     edi, -1
    shr     edi, cl
.LBB0_5:                                #   Parent Loop BB0_1 Depth=1
    lea     ecx, [rbx + rbx]
    sar     ebx, 31
    and     ebx, -1522885655
    xor     ebx, ecx
    mov     ecx, ebx
    and     ecx, edi
    cmp     rsi, rcx
    jbe     .LBB0_5
    add     ecx, edx
    mov     sil, byte ptr [rsp + rdx]
    movsxd  rdi, ecx
    mov     cl, byte ptr [rsp + rdi]
    mov     byte ptr [rsp + rdx], cl
    mov     byte ptr [rsp + rdi], sil
    add     rdx, 1
    cmp     rdx, rax
    jne     .LBB0_4

【讨论】:

  • 我有一个错误“'__builtin_clz':找不到标识符”。我该如何解决这个问题?
  • 我猜这取决于你的编译器。如果您不使用 gcc/clang,则可以使用其中一种小技巧,例如:graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
  • int mask = limit - 1;掩码 |= 掩码 >> 1;掩码 |= 掩码 >> 2;掩码 |= 掩码 >> 4;掩码 |= 掩码 >> 8;掩码 |= 掩码 >> 16;
  • 我只是按照您的建议替换“掩码”值并运行代码。生成 10000 个随机字符串仍然需要 6-7 秒,如果为它设置特定的字符串长度,则需要更长的时间。我在上面使用了“parallel_for”,但性能并没有好转
  • 这可能有很多原因,如上所述。要点是检查汇编输出,看看它是否和上面的代码一样短。很大程度上取决于您的编译器和设置。实验并学习以获得所需的结果。
猜你喜欢
  • 1970-01-01
  • 2016-12-04
  • 1970-01-01
  • 2021-11-18
  • 1970-01-01
  • 1970-01-01
  • 2010-11-05
  • 2023-04-01
  • 1970-01-01
相关资源
最近更新 更多