我正在编写一个OpenGL3 2D引擎。 目前,我正在努力解决瓶颈问题。因此,AMD Profiler的以下输出: http://h7.abload.de/img/profilerausa.png
数据是使用数千个精灵制作的。
然而,在50.000精灵上,testapp已经无法以5 fps的速度使用。
这表明,我的瓶颈是我使用的转换功能。 那是相应的功能: http://code.google.com/p/nightlight2d/source/browse/NightLightDLL/NLBoundingBox.cpp#130
void NLBoundingBox::applyTransform(NLVertexData* vertices)
{
if ( needsTransform() )
{
// Apply Matrix
for ( int i=0; i<6; i++ )
{
glm::vec4 transformed = m_rotation * m_translation * glm::vec4(vertices[i].x, vertices[i].y, 0, 1.0f);
vertices[i].x = transformed.x;
vertices[i].y = transformed.y;
}
m_translation = glm::mat4(1);
m_rotation = glm::mat4(1);
m_needsTransform = false;
}
}
我不能在着色器中执行此操作,因为我正在同时批处理所有精灵。这意味着,我必须使用CPU来计算变换。
我的问题是: 解决这个瓶颈的最佳方法是什么?
我不使用任何线程atm,所以当我使用vsync时,我也会获得额外的性能,因为它等待屏幕完成。这告诉我应该使用线程。
另一种方法是使用OpenCL吗?我想避免使用CUDA,因为据我所知它只能在NVIDIA显卡上运行。是对的吗?
post scriptum:
如果您愿意,可以在此处下载演示:
http://www63.zippyshare.com/v/45025690/file.html
请注意,这需要安装VC ++ 2008,因为它是运行探查器的调试版本。
答案 0 :(得分:4)
我要做的第一件事是在你进入for循环之前将旋转和变换矩阵连接成一个矩阵......这样你就不会在每个for循环上计算两个矩阵乘法和一个向量;相反,你只会乘以一个向量和矩阵。其次,您可能希望展开展开循环,然后使用更高的优化级别进行编译(在g++
我至少会使用-O2
,但我不熟悉MSVC,所以你会必须自己翻译优化级别)。这样可以避免代码中的分支可能产生的任何开销,尤其是在缓存刷新时。最后,如果您还没有研究过它,请检查一下SSE优化,因为您正在处理向量。
更新:我将添加一个涉及线程的最后一个想法......当你执行线程时基本上管道你的顶点。例如,假设您有一台具有八个可用CPU线程的计算机(即具有超线程的四核)。为顶点管道处理设置六个线程,并使用非锁定单用户/生成器队列在管道的各个阶段之间传递消息。每个阶段将转换六个成员顶点数组的单个成员。我猜这些六个成员的顶点数组中有一堆,所以在流中通过管道设置,你可以非常有效地处理流,并避免使用互斥锁和其他锁定信号等。有关快速非锁定单生产者/消费者队列see my answer here的更多信息。
更新2 :你只有一个双核处理器...所以转储管道的想法,因为它会在每个线程争用CPU资源时遇到瓶颈。
答案 1 :(得分:2)
我不能在着色器中执行此操作,因为我正在同时批处理所有精灵。这意味着,我必须使用CPU来计算变换。
这听起来很可疑,就像你所做的过早优化一样,假设批处理是你可以做的最重要的事情,因此你构建你的渲染器来进行最少数量的绘制调用。而现在它又回来咬你了。
您需要做的是没有更少的批次。您需要拥有正确数量的批次。当你放弃GPU顶点转换以支持CPU转换时,你知道你已经进行了批处理。
正如Datenwolf建议的那样,你需要进行一些实例化才能在GPU上重新获得转换。但即使这样,你也需要撤消一些你在这里过度批量处理的事情。你还没有谈到你正在渲染什么样的场景(顶部有精灵的瓷砖地图,大型粒子系统等),所以很难知道建议什么。
此外,GLM是一个很好的数学库,但不是为了获得最佳性能而设计的。如果我需要每帧在CPU上转换300,000个顶点,通常不会使用它。
答案 2 :(得分:1)
循环中的赋值可能是个问题,但我对这个库并不熟悉。将它移到for循环之外,手动执行字段分配可能会有所帮助。在环路外移动转换也会有所帮助。
编辑:
这更符合我的想法。
// Apply Matrix
glm::vec4 transformed;
glm::mat4 translation = m_rotation * m_translation;
for ( int i=0; i<6; i++ )
{
transformed.x = vertices[i].x;
transformed.y = vertices[i].y;
transformed.z = vertices[i].z;
transformed.w = 1.f; // ?
/* I can't find docs, but assume they have an in-place multiply
transformed.mult(translation);
// */
vertices[i].x = transformed.x;
vertices[i].y = transformed.y;
}
也许,只是可能,这项任务使编译器无法内联或展开某些内容。我有点猜测乘法很大,足以将其从指令缓存中删除。实际上,如果你开始谈论缓存的大小,你就不会在许多平台上具有弹性。
您可以尝试复制一些堆栈并制作更多更小的循环。
glm::vec4 transformed[6];
for (size_t i = 0; i < 6; i++) {
transformed[i].x = vertices[i].x;
transformed[i].y = vertices[i].y;
transformed[i].z = vertices[i].z;
transformed.w = 1.f; // ?
}
glm::mat4 translation = m_rotation * m_translation;
for (size_t i = 0; i < 6; i++) {
/* I can't find docs, but assume they have an in-place multiply
transformed.mult(translation);
// */
}
for (size_t i = 0; i < 6; i++) {
vertices[i].x = transformed[i].x;
vertices[i].y = transformed[i].y;
}
正如Jason所说,手动展开这些循环可能会很有趣。
我真的不认为你会看到任何这些变化都会有一个数量级的改进。
我怀疑减少调用此函数比使此函数更快更重要。
这个事实是你在这个函数中有这个needTransform检查当您在低级别代码中遇到类似这样的高级别问题时,您最终会盲目地一味地调用此方法,认为它是免费的。无论你对变换需求的假设是否属实,都可能非常不正确。
现实情况是,您应该只是调用此方法一次。当您想要applyTransform时,您应该applyTransform。当可能想要applyTransform时,您不应该调用applyTransform。接口应该是一个契约,对它们进行处理。
答案 3 :(得分:1)
如果你坚持在CPU上进行计算,你应该自己做数学。
现在,您在2D环境中使用4x4矩阵,其中一个2x2矩阵用于旋转,一个简单的矢量用于翻译就足够了。这是轮换的4次乘法和4次加法,以及两次翻译补充。
如果你绝对需要两个矩阵(因为你需要结合平移和旋转),它仍然比你现在的要少得多。但是你也可以通过移动矢量的位置,旋转然后再将它移回来“手动”组合这两个,这可能比乘法快一点,虽然我不确定。
与那些4x4矩阵现在所做的操作相比,这个操作要少得多。