【发布时间】:2020-07-06 03:58:30
【问题描述】:
我正在尝试在 C++ 中分析我的 OpenCL 内核的性能。 我目前使用 std::chrono 每次在排队之前保存开始时间,之后调用 cl::finish() 然后保存结束时间。 虽然大多数结果看起来很准确,但我发现一个缓冲区的读取时间比所有其他缓冲区要长。
如果您查看我在下面的屏幕截图中登录的时间,您会发现“Next Index”比“Vertex”、“Normal”或“Tri”花费的时间要长得多(~770ms vs 50-200ms)。 (这些是 4 个读取缓冲区,我入队)。 这很奇怪,因为“Vertex”和“Normal”拥有浮点数(至少应该与 int32 具有相同的内存?)并且大小为 1.5,而“Next Index”则拥有 int32s。 尤其是“Tri”也持有 int32s,与“Next Index”的 770ms 相比只需要 54ms(虽然公平地说,它只有“Next Index”的大小)。
现在我认为这是因为内核在登录时并未实际执行。正如您所看到的,它们基本上根本不需要毫秒,当它们的计算量非常大并且“下一个索引”是第一个 ReadBuffer 时,它基本上承担了所有责任。 所以我认为问题不在于“下一个索引”,而在于记录内核。 我发现了这个:https://community.khronos.org/t/clenqueuereadbuffer-is-incredibly-slow-when-called-infrequently/7325 答案表明 cl::flush() 应该在“enqueueNDRangeKernel”之后调用,所以 CPU 实际上会等到内核完成(我认为 cl::finish() 已经完成了?)但是如果我将“flush”换成“完成”我仍然得到相同的结果。
那么有没有人知道如何在这种情况下测量内核性能? 我显然只能排队一个内核和一个非常小的读取缓冲区,然后在后者完成后进行测量以获得内核执行时间,但我想要一个更清洁的解决方案,所以我可以随时测试,而无需每次都修改很多代码。
下面我还发布了我对内核和缓冲区进行排队的方式以及我如何记录时间:
// queue Kernel execution, calculate a whole cube per work item
queue.enqueueNDRangeKernel(marchingCubesKernel, cl::NullRange, cl::NDRange(cubeCount));
cl::finish();
auto enqueue1End = std::chrono::high_resolution_clock::now();
auto enqueue2Start = std::chrono::high_resolution_clock::now();
// enqueue one kernel per vertex to search for next viable vertex in array
queue.enqueueNDRangeKernel(cleanUpKernel, cl::NullRange, cl::NDRange(vertexCount));
cl::finish();
auto enqueue2End = std::chrono::high_resolution_clock::now();
auto enqueueReadStart = std::chrono::high_resolution_clock::now();
// Read buffer back into vectors
auto nextIndexStart = std::chrono::high_resolution_clock::now();
queue.enqueueReadBuffer(nextIndexBuf, CL_FALSE, 0, sizeof(int32) * nextIndex.size(), nextIndex.data());
cl::finish();
auto nextIndexEnd = std::chrono::high_resolution_clock::now();
auto vertexStart = std::chrono::high_resolution_clock::now();
queue.enqueueReadBuffer(vertexBuf, CL_FALSE, 0, sizeof(float) * verCoords.size(), verCoords.data());
cl::finish();
auto vertexEnd = std::chrono::high_resolution_clock::now();
auto normalStart = std::chrono::high_resolution_clock::now();
queue.enqueueReadBuffer(normalBuf, CL_FALSE, 0, sizeof(float) * verNormalCoords.size(), verNormalCoords.data());
cl::finish();
auto normalEnd = std::chrono::high_resolution_clock::now();
auto triStart = std::chrono::high_resolution_clock::now();
queue.enqueueReadBuffer(triangleBuf, CL_FALSE, 0, sizeof(int32) * tris.size(), tris.data());
cl::finish();
auto triEnd = std::chrono::high_resolution_clock::now();
// wait till queue is empty
cl::finish();
auto enqueueReadEnd = std::chrono::high_resolution_clock::now();
auto end = std::chrono::high_resolution_clock::now();
double timeTaken = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
double kernel1Time = std::chrono::duration_cast<std::chrono::milliseconds>(enqueue1End - enqueue1Start).count();
double kernel2Time = std::chrono::duration_cast<std::chrono::milliseconds>(enqueue2End - enqueue2Start).count();
double readTime = std::chrono::duration_cast<std::chrono::milliseconds>(enqueueReadEnd - enqueueReadStart).count();
UE_LOG(LogTemp, Warning, TEXT("Cube March took: %f ms, consisting of:"), timeTaken);
UE_LOG(LogTemp, Warning, TEXT("Kernel1 took: %f ms"), kernel1Time);
UE_LOG(LogTemp, Warning, TEXT("Kernel2 took: %f ms"), kernel2Time);
UE_LOG(LogTemp, Warning, TEXT("Reading took: %f ms"), readTime);
double nextIndexTime = std::chrono::duration_cast<std::chrono::milliseconds>(nextIndexEnd - nextIndexStart).count();
UE_LOG(LogTemp, Warning, TEXT("Next Index took: %f ms"), nextIndexTime);
double vertexTime = std::chrono::duration_cast<std::chrono::milliseconds>(vertexEnd - vertexStart).count();
UE_LOG(LogTemp, Warning, TEXT("Vertex Time took: %f ms"), vertexTime);
double normalTime = std::chrono::duration_cast<std::chrono::milliseconds>(normalEnd - normalStart).count();
UE_LOG(LogTemp, Warning, TEXT("Normal Time took: %f ms"), normalTime);
double triTime = std::chrono::duration_cast<std::chrono::milliseconds>(triEnd - triStart).count();
UE_LOG(LogTemp, Warning, TEXT("Tri Time took: %f ms"), triTime);
如果有人有想法,请告诉我。 这不是一个大问题,但我想了解为什么完成和刷新似乎不适用于内核。 提前致谢, 美食家
【问题讨论】:
-
您是使用
makeDefault()创建CommandQueue,还是至少使用makeDefaultProvided()将创建的CommandQueue设置为默认值?您不检查cl::finish()返回的内容 - 这实际上可能是一个错误。你为什么不直接使用queue.finish(),这样很明显你实际上是在当前队列上调用finish()?或者,您可以通过设置CL_TRUE将queue.enqueueReadBuffer()切换为阻塞调用,然后在每次读取数据后无需使用finish()。 -
您好,感谢您的回复!我找不到任何用于 CommandQueue 的 makeDefault() 方法,但只能用于 DeviceCommandQueue。如果我只使用一个队列,这是否必要? queue.finish() 没有改变任何东西,但我会确保从现在开始使用那个。您的评论也确实解决了我的问题!将 cl::finish 保存到 err 变量时我没有发现任何错误,但由于某种原因使它工作!现在一切都正确记录了,谢谢!我无法将您的评论标记为解决方案,您是想为这些点提供答案(不知道它是如何工作的)还是我应该自己回答?
-
cl::finish()在非默认队列上什么都不做。我只是把它作为一个答案。如果你高兴就接受它。谢谢。