【问题标题】:Reduce mutex overhead in multi-threading voxel algorithms减少多线程体素算法中的互斥量开销
【发布时间】:2014-04-23 04:53:47
【问题描述】:

我目前正在实现一个多线程体素游戏引擎。当我使用多线程时,我很快就遇到了互斥锁的性能瓶颈。

为了澄清我的问题,让我们来看一个 2D 案例:

+-+-+-+
|A|B|C|
+-+-+-+
|D|E|F|
+-+-+-+
|G|H|I|
+-+-+-+

所有这些单元都是体素块(16x16 体素)。

我使用多个线程以块为基础执行所有体素算法。我有一个由要处理的块组成的作业队列,每个工作线程只是不断地从队列中挑选块并对其进行处理。

现在假设一个线程需要在块 E 中进行一些光照计算。因为在 E 的一个角落可能有一个光源会传播到相邻的块,它必须锁定所有九个相邻的块以避免潜在的数据竞争,使用互斥锁。

但是,正如我所试验的那样,互斥体的性能开销并不好。目前我正在使用一个简单的for 循环来添加作业。所以当游戏运行时,初始作业队列会变成这样:

A, B, C, D, E, F, G, H, I, ...

这真的很糟糕,因为第一个作业 A 将锁定 A、B、D、E,并使后面的八个作业都等待互斥锁,从而降低性能。

目前我能想到的唯一缓解方法是尝试以分散的方式添加作业,希望我们可以避免大多数停顿。但我不喜欢这种方法,因为它看起来更像是一种解决方法,而且如果锁定模式发生变化,它也不是很灵活。

我也想过使用“异步互斥锁”。但我不太确定该怎么做。

编辑: 澄清一下,照明作业是在运行时添加的,而不是按固定顺序添加的。例如,假设玩家从当前处理的区块中移出,那么只有外面的区块应该被添加到队列中,队列可能处于不规则的边界。

所以我认为仅仅使用一个像样的调度器是不足以解决这个问题的。

【问题讨论】:

  • 假设您的体素块不是太大,是否可以复制受影响的块,处理它们并在最后合并结果?例如。线程 1 拉出块 A,发现它需要在块 C 和 F 上执行光照。然后它复制 C 和 F 并继续处理它们。线程 2 拉出块 B 并看到还需要在 C 上工作。它还复制 C 并对其进行工作。处理完所有块后,主线程从每个线程获取结果并执行加法混合以获得最终的光照贴图。
  • @Wes 聪明的主意。但是我使用的大多数体素算法(包括照明)应该具有接近 O(N) 级别的性能(N 是块大小)。在主线程中访问所有块的附加步骤只会使多线程无用。
  • 整个代码块是互斥的吗?也就是说,如果您在A 运行时无法为B 做任何事情,只需将B 移动到队列的后面并获取下一个项目。当然,如果您已经为B 做了一些工作,这可能不起作用。我猜你不想回滚工作。
  • @Heuster:是互斥的。或者至少我必须像@Wes 所说的那样做额外的混合步骤。你的方法应该在理论上有效。但是如何存储和获取谁在处理哪些块的信息呢?在上图中,我按顺序锁定互斥锁(例如,对于E,锁定ABC,...,I,一个接一个)。因为我们不能一次原子地锁定所有九个互斥锁,而这种方案可以避免死锁。如果它没有互斥锁,或者可能使用 mutex.tryLock(),如何避免死锁(以及潜在的活锁)?
  • 好的,这样的碰撞多久发生一次?我知道这样的事件一定不能发生,但如果它们相对不频繁,应该可以设计一个更好的防冲突策略,在非冲突情况下避免九个互斥锁,但在实际检测到冲突时需要更多操作.这将提高整体性能。

标签: c++ multithreading algorithm mutex voxel


【解决方案1】:

这太长了,不适合 cmets 部分。

如何使用原子布尔值来查看当前是否正在处理块?这样,您将获得更高的线程利用率,而不是等待处理的线程。此外,如果您在相同分散的点启动每个线程,您将获得更少的冲突。当线程需要在相同的块上工作时,这个算法就解决了这个问题。

  1. 线程 1 获取块 A(影响块 B 和 C)并设置 A 的处理标志。
  2. 线程 2 同时获取块 B 并设置 B 的处理标志。
  3. 线程 1 完成块 A 并重置 A 的标志并检查块 B 是否可用。
  4. 线程 2 正忙于处理 B,因此线程 1 暂时让 B 处于未处理状态,然后移至 C 并设置 C 的标志。
  5. 线程 2 结束,B 重置 B 的标志。
  6. 线程 1 完成 C 并重置 C 的标志,然后移动到 B。B 已经有线程 2 的结果,因此线程 1 对 B 进行最后一次传递并完成。

【讨论】:

  • 问题是我当前的照明算法需要所有 9 个块都可用并一次处理所有块。它不能以部分和可恢复的方式进行。不仅照明算法,而且我正在使用的一些程序生成算法都需要这种行为(需要所有相邻的块才能工作)。使用这种方法会使编码变得更加困难。
【解决方案2】:

如何在线程之间进行一些偏移的调度:

  • 不同的颜色代表不同的工作线程
  • 图像代表地图中的单行
  • 更多内容见下文

您有 4 或 8 个邻居,例如 4 个工作线程

  • 为清楚起见,单元格 0102 表示 row=01 列 - 02(全部从 0 开始计数)

    thread 0000 00001 0002 0003
    -------------------------
           cell cell cell cell
    -------------------------
           0000 0003 0006 0009
           0001 0004 0007 0010
           0002 0005 0008 0011
    
           0012 0015 0018 0021
           0013 0016 0019 0022
           0014 0017 0020 0023
    
           0024 0027 0030 0033
           0025 0028 0030 0034
           0026 0029 0030 0035
    
           ... till end of row
    
           0100 0103 0106 0109
           0101 0104 0107 0110
           0102 0105 0108 0111
    
           0112 0115 0118 0121
           0113 0116 0119 0122
           0114 0117 0120 0123
    
           0124 0127 0130 0133
           0125 0128 0130 0134
           0126 0129 0130 0135
    
           ... till end of row
           ... till end map
    
  • 线程间距越大越好

  • 最少2个空格
  • 当到达行尾时,线程应该等待全部完成,然后再调度下一行
  • 您也可以完全避免在先卸除间隙边缘然后再卸除其余部分时发生冲突
    • 而不是 0000,0001,0002 将是 0002,0000,0001

这种方法会产生数据传播工件!!!

  • 因为数据不会在邻居之间持续传播
  • 因此可能会出现一些类似网格的伪影...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-12-27
    • 2023-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-08
    • 2016-04-15
    • 1970-01-01
    相关资源
    最近更新 更多