65.9K
CodeProject 正在变化。 阅读更多。
Home

灵活粒子系统 - 代码优化

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2014 年 10 月 7 日

CPOL

6分钟阅读

viewsIcon

7434

使我的粒子系统运行得更快的几项代码更改:SIMD、随机数生成器、指针别名、内存对齐

performance

在玩弄了这些工具之后,我们有了更多选项来提高粒子系统的性能。这一次,我们需要重写部分代码。

总的来说,粒子系统的运行速度比最初快了将近**两倍**!请继续阅读,了解哪些代码片段得到了修改。

系列文章

本文计划

Start

我们从这些数字开始,查看上一篇文章(上次结果)

Core i5 Sandy Bridge

计数 隧道 吸引子 喷泉
171000 429.195 608.598 460.299
181000 460.649 647.825 490.412
191000 489.206 688.603 520.302

Core i5 Ivy Bridge

计数 隧道 吸引子 喷泉
171000 529.188 746.594 570.297
181000 565.648 792.824 605.912
191000 593.956 832.478 640.739

(时间单位为毫秒)

SIMD 准备

之前,我尝试强制编译器使用 SSE2 或 AVX 指令。正如我们所见,这带来了不错的性能提升(AVX 大约 10%)。但是……SIMD 应该能以 4 倍或 8 倍的速度计算,那为什么我们只获得了微小的提升?

在现实生活中,事情并非如此简单

  • SIMD 一次可以执行 4 或 8 条指令,但我们仍然需要等待内存。有关更多信息,请参阅我对讲座 "现代 CPU 上的原生代码性能" 的总结。总的来说,假设我们的代码是理想的“可向量化”代码,使用 SSE2/4 最多可以获得 2.5 倍的速度提升。并非所有代码都处于如此完美的状态。
  • 当前的 CPU 是超标量的,这意味着 CPU 可以并行运行多个不同的指令。有时,SIMD 代码的速度甚至会比编译器生成的原始代码还要慢。
  • 另一个小问题:SIMD 寄存器需要 128 位(16 字节对齐)的内存块。我们在分配新内存时需要注意这一点。因此,并非所有变量或数组都适合 SSE 代码。

我们能做什么?

  • 由于粒子主要操作 glm::vec4,因此很有可能利用 SSE 的全部功能。我们每个向量使用 4 个浮点数,即 16 字节。
  • glm 添加了一个非常好的功能 glm::simdVec4,它基本上为常见的向量函数添加了 SSE 代码。所以我只是将 glm::vec4 改为了 glm::simdVec4
  • 内存必须对齐,所以我使用了 _aligned_malloc_aligned_free

一些代码示例

// particles.h, in ParticleData class declaration
glm::simdVec4 *m_pos;
glm::simdVec4 *m_col;

// in particles.cpp, generate() method:
m_pos = (glm::simdVec4 *)_aligned_malloc(sizeof(glm::vec4)*maxSize, 16);
m_col = (glm::simdVec4 *)_aligned_malloc(sizeof(glm::vec4)*maxSize, 16);

// particles.cpp, destructor
_aligned_free(m_pos);
_aligned_free(m_col);

更改后的结果(Visual Studio)

Sandy Bridge

计数 隧道 吸引子 喷泉
171000 387.563 495.281 394.641
181000 417.320 529.660 426.330
191000 447.665 563.833 450.416

Ivy Bridge

计数 隧道 吸引子 喷泉
171000 476.625 596.313 483.656
181000 514.328 639.664 523.332
191000 552.666 682.333 558.667

哇:提升了近20%!这都是通过正确的数据结构(用于向量)和内存对齐实现的。

SSE 和 AVX 指令

到目前为止,我们获得了不错的加速……现在,让我们为最关键的循环编写一些 SSE 代码。它会运行得更快吗?

欧拉更新,SSE

__m128 ga = globalA.Data;
__m128 *pa, *pb, pc;
__m128 ldt = _mm_set_ps1(localDT);

size_t i;
for (i = 0; i < endId; i++)
{
    pa = (__m128*)(&p->m_acc[i].x);
    *pa = _mm_add_ps(*pa, ga);
}

for (i = 0; i < endId; i ++)
{
    pa = (__m128*)(&p->m_vel[i].x);
    pb = (__m128*)(&p->m_acc[i].x);
    pc = _mm_mul_ps(*pb, ldt);
    *pa = _mm_add_ps(*pa, pc);
}

for (size_t i = 0; i < endId; i++)
{
    pa = (__m128*)(&p->m_pos[i].x);
    pb = (__m128*)(&p->m_vel[i].x);
    pc = _mm_mul_ps(*pb, ldt);
    *pa = _mm_add_ps(*pa, pc);
}

在这种情况下,可读性大大降低。

结果

Sandy Bridge

计数 隧道 吸引子 喷泉
171000 386.453 492.727 393.363
181000 416.182 529.591 423.795
191000 444.398 564.199 450.099

Ivy Bridge

计数 隧道 吸引子 喷泉
171000 481.172 584.086 486.543
181000 516.271 623.136 514.068
191000 547.034 656.517 541.258

不幸的是,提升不大。这是因为 glm::simdVec4 使用了 SSE 代码。所以没有必要重写它。我们会失去可读性,并且性能提升是值得怀疑的。

指针别名:__restrict 关键字

在我上一篇文章中,我收到了来自 Matías N. Goldberg 的一个非常有趣的评论

……根据我的经验,您可以通过告诉编译器指针之间不会发生别名来获得**巨大的**性能提升……

Matias 建议使用 __restrict 关键字来告诉编译器指针不会发生别名。例如

glm::vec4 * __restrict acc = p->m_acc;
glm::vec4 * __restrict vel = p->m_vel;
glm::vec4 * __restrict pos = p->m_pos;

然后,使用 pos 指针而不是 p->m_pos

当我将所有更新器(和生成器)代码进行此类更改后,我得到了以下结果

Sandy Bridge

计数 隧道 吸引子 喷泉
171000 372.641 476.820 376.410
181000 401.705 508.353 404.176
191000 427.588 542.794 432.397

Ivy Bridge

计数 隧道 吸引子 喷泉
171000 475.609 591.805 480.402
181000 502.201 620.601 512.300
191000 534.150 667.575 541.788

这不是巨大的提升,但仍然值得测试。

随机数生成器

目前,调用的是标准的 C rand() 函数。对于粒子系统,我们可能不需要使用更高级的东西(例如正态分布随机生成器)——均匀分布就可以了……也许有一些比默认生成器更快的生成器?到目前为止,我主要关注了更新器部分。但是,生成器也可以稍作改进。在此模块中,随机数生成器被大量使用。如果我们更改它呢?

我搜索了一下,在这里、这里这里找到了类似的东西。

我尝试使用这个生成器

// http://www.rgba.org/articles/sfrand/sfrand.htm
static unsigned int mirand = 1;
float sfrand(void) {
    unsigned int a;
    mirand *= 16807;
    a = (mirand & 0x007fffff) | 0x40000000;
    return(*((float*)&a) - 3.0f);
}

它具有均匀分布和 23 位精度(C rand() 只有 16 位)。

结果

Sandy Bridge

计数 隧道 吸引子 喷泉
171000 334.633 443.816 348.908
181000 363.954 474.477 372.739
191000 384.869 501.435 394.217

Ivy Bridge

计数 隧道 吸引子 喷泉
171000 412.172 531.586 429.293
181000 450.146 573.073 463.037
191000 473.518 606.759 484.880

哇!现在 Sandy Bridge 的总提升约为 28%,Ivy Bridge 几乎相同。

总结

最终结果

CPU 计数 隧道 吸引子 喷泉
Sandy 191000 384.869 (-21.3%) 501.435 (-27.2%) 394.217 (-24.2%)
Ivy 191000 473.518 (-20.3%) 606.759 (-27.1%) 484.880 (-24.3%)

总计(以工具优化前的耗时为基准)

CPU 隧道 吸引子 喷泉
Sandy 35.5% 43,5% 39,7%
Ivy 33.2% 38,2% 35,6%

我们可以“反转”这些数字,说现在的吸引子效果运行速度快了近两倍!不算差!

结论

  • 内存对齐和正确的数据结构是关键因素。
  • 仅在需要时编写 SIMD 代码,通常最好依赖编译器和第三方库。
  • 更好地描述您的代码:例如使用 __restrict 关键字。这样,编译器就可以生成更好的代码。
  • 随机数生成器可能会带来差异。

下一步

到目前为止,渲染器非常简单。也许有一些方法可以改进它的代码。当然,我们必须关注 CPU 到 GPU 的内存传输以及更好的缓冲区使用。

参考文献

© . All rights reserved.