我正在编写一个OpenGL3 2D引擎。目前,我正在尝试解决一个瓶颈。因此,请输入AMD Profiler的以下输出:http://h7.abload.de/img/profilerausa.png
这些数据是用几千个精灵制作的。
然而,在50.000个精灵的情况下,测试应用在5fps时就已经无法使用了。
这表明,我的瓶颈是我使用的转换函数。这就是对应的函数: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来计算变换。
我的问题是:解决这个瓶颈的最佳方法是什么?
我没有使用任何线程自动柜员机,所以当我使用vsync时,我也会得到额外的性能影响,因为它会等待屏幕完成。这告诉我应该使用线程。
另一种方法可能是使用OpenCL?我想避免CUDA,因为据我所知,它只能在NVIDIA卡上运行。是那么回事吗?
后脚本:
如果你喜欢,你可以在这里下载一个演示:
http://www63.zippyshare.com/v/45025690/file.html
请注意,这需要安装VC++2008,因为它是用于运行分析器的调试版本。
发布于 2011-08-03 03:12:15
在进入for循环之前,我要做的第一件事就是将旋转和矩阵转换成一个矩阵。这样,您就不需要在每个for循环上计算两个矩阵乘法和一个向量;相反,您将只计算一个向量和一个矩阵的乘法。其次,您可能希望展开循环,然后使用更高的优化级别进行编译(在g++上,我至少会使用-O2,但我不熟悉MSVC,因此您必须自己转换优化级别)。这将避免代码中的分支可能产生的任何开销,特别是在缓存刷新时。最后,如果您还没有研究过它,请检查一些SSE优化,因为您正在处理向量。
更新:我将添加最后一个涉及线程化的想法...基本上,当你进行线程时,你的顶点是流水线的。例如,假设您有一台机器有8个可用的CPU线程(即具有超线程的四核)。为顶点管道处理设置六个线程,并使用非锁定的单消费者/生产者队列在管道的各个阶段之间传递消息。每个阶段都会变换六元顶点数组中的一个成员。我猜有一堆这样的六成员顶点数组,所以在流中设置通过管道传递的流,您可以非常有效地处理流,并避免使用互斥和其他锁定信号量,等等。有关快速的非锁单生产者/消费者队列的更多信息,see my answer here。
UPDATE 2:您只有一个双核处理器...因此,丢弃管道的想法,因为它将遇到瓶颈,因为每个线程都会争用CPU资源。
发布于 2011-08-03 06:17:06
我不能在着色器中这样做,因为我一次批处理所有的精灵。这意味着,我必须使用CPU来计算变换。
这听起来像是您过早地进行了优化,假设批处理是您可以做的最重要的事情,因此您围绕进行最少数量的绘制调用来构建渲染器。现在它又回来咬你了。
你需要做的不是更少的批次。您需要有正确的批次数量。当你放弃GPU顶点变换而支持CPU变换时,你知道你在批处理方面走得太远了。
正如Datenwolf建议的那样,您需要进行一些实例化操作,以便将转换返回到GPU上。但即使这样,您也需要取消这里的一些过度批处理。您并没有过多地谈论您正在渲染的场景类型(顶部带有精灵的平铺地图、大型粒子系统等),因此很难知道该建议什么。
此外,GLM是一个很好的数学库,但它不是为实现最高性能而设计的。如果我需要在CPU上每帧变换300,000个顶点,我通常不会使用它。
发布于 2011-08-03 03:28:30
循环中的赋值可能是个问题,不过我对这个库并不熟悉。将其移出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提到的,手动展开这些循环可能很有趣。
不过,我真的不认为你会看到这些变化会有一个数量级的改善。
我怀疑少调用这个函数比让这个函数更快更重要。在这个函数中有这个needsTransform检查的事实让我认为这可能是相关的。
当您在低级代码中有这样的高级关注点时,您最终只是盲目地一遍又一遍地调用此方法,并认为它是免费的。无论你对needsTransform为真的频率的假设是否正确,都可能是非常错误的。
实际情况是,您应该只调用此方法一次。当你想applyTransform的时候,你应该applyTransform。当你想applyTransform的时候,你不应该调用applyTransform。接口应该是一种契约,就像对待它们一样。
https://stackoverflow.com/questions/6917206
复制相似问题