Ich habe angefangen, OpenCL zu lernen und ich versuche derzeit zu testen, wie sehr ich die Leistung für einen einfachen Skelettanimationsalgorithmus verbessern kann. Um dies zu tun, habe ich ein Programm geschrieben, das Skelettanimation aus zufällig generierten Scheitelpunkten und Transformationsmatrizen zweimal durchführt, einmal mit einer SSE-optimierten linearen Algebra-Bibliothek in reinem C++ und einmal mit meinem eigenen OpenCL-Kernel auf GPU (ich teste auf einem Nvidia GTX 460).OpenCL Performance Optimization
Ich begann mit einem einfachen Kernel, bei dem jedes Arbeitselement genau einen Knoten transformiert, wobei alle Werte aus dem globalen Speicher gelesen werden. Da ich mit der Leistung dieses Kernels nicht zufrieden war, versuchte ich ein wenig zu optimieren. Mein aktueller Kernel sieht wie folgt aus:
inline float4 MultiplyMatrixVector(float16 m, float4 v)
{
return (float4) (
dot(m.s048C, v),
dot(m.s159D, v),
dot(m.s26AE, v),
dot(m.s37BF, v)
);
}
kernel void skelanim(global const float16* boneMats, global const float4* vertices, global const float4* weights, global const uint4* indices, global float4* resVertices)
{
int gid = get_global_id(0);
int lid = get_local_id(0);
local float16 lBoneMats[NUM_BONES];
async_work_group_copy(lBoneMats, boneMats, NUM_BONES, 0);
barrier(CLK_LOCAL_MEM_FENCE);
for (int i = 0 ; i < NUM_VERTICES_PER_WORK_ITEM ; i++) {
int vidx = gid*NUM_VERTICES_PER_WORK_ITEM + i;
float4 vertex = vertices[vidx];
float4 w = weights[vidx];
uint4 idx = indices[vidx];
resVertices[vidx] = (MultiplyMatrixVector(lBoneMats[idx.x], vertex * w.x)
+ MultiplyMatrixVector(lBoneMats[idx.y], vertex * w.y)
+ MultiplyMatrixVector(lBoneMats[idx.z], vertex * w.z)
+ MultiplyMatrixVector(lBoneMats[idx.w], vertex * w.w));
}
}
Jetzt habe ich eine konstante Anzahl von Eckpunkten pro Arbeitsposten verarbeiten, und ich Prefetch alle Knochen Matrizen in den lokalen Speicher nur einmal für jede Arbeitsposition, die ich glaubte, würde dazu führen, zu einer besseren Leistung, da die Matrizen für mehrere Vertices anschließend aus dem schnelleren lokalen Speicher gelesen werden können. Leider ist dieser Kernel schlechter als mein erster Versuch und sogar schlechter als die reine CPU-Implementierung.
Warum ist die Leistung so schlecht mit dieser sollte Optimierung sein?
Wenn es hilft, ist hier, wie ich den Kernel ausführen:
#define NUM_BONES 50
#define NUM_VERTICES 30000
#define NUM_VERTICES_PER_WORK_ITEM 100
#define NUM_ANIM_REPEAT 1000
uint64_t PerformOpenCLSkeletalAnimation(Matrix4* boneMats, Vector4* vertices, float* weights, uint32_t* indices, Vector4* resVertices)
{
File kernelFile("/home/alemariusnexus/test/skelanim.cl");
char opts[256];
sprintf(opts, "-D NUM_VERTICES=%u -D NUM_REPEAT=%u -D NUM_BONES=%u -D NUM_VERTICES_PER_WORK_ITEM=%u", NUM_VERTICES, NUM_ANIM_REPEAT, NUM_BONES, NUM_VERTICES_PER_WORK_ITEM);
cl_program prog = BuildOpenCLProgram(kernelFile, opts);
cl_kernel kernel = clCreateKernel(prog, "skelanim", NULL);
cl_mem boneMatBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_BONES*sizeof(Matrix4), boneMats, NULL);
cl_mem vertexBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*sizeof(Vector4), vertices, NULL);
cl_mem weightBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*4*sizeof(float), weights, NULL);
cl_mem indexBuf = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, NUM_VERTICES*4*sizeof(uint32_t), indices, NULL);
cl_mem resVertexBuf = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY | CL_MEM_ALLOC_HOST_PTR, NUM_VERTICES*sizeof(Vector4), NULL, NULL);
uint64_t s, e;
s = GetTickcount();
clSetKernelArg(kernel, 0, sizeof(cl_mem), &boneMatBuf);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &vertexBuf);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &weightBuf);
clSetKernelArg(kernel, 3, sizeof(cl_mem), &indexBuf);
clSetKernelArg(kernel, 4, sizeof(cl_mem), &resVertexBuf);
size_t globalWorkSize[] = { NUM_VERTICES/NUM_VERTICES_PER_WORK_ITEM };
size_t localWorkSize[] = { NUM_BONES };
for (size_t i = 0 ; i < NUM_ANIM_REPEAT ; i++) {
clEnqueueNDRangeKernel(cq, kernel, 1, NULL, globalWorkSize, localWorkSize, 0, NULL, NULL);
}
clEnqueueReadBuffer(cq, resVertexBuf, CL_TRUE, 0, NUM_VERTICES*sizeof(Vector4), resVertices, 0, NULL, NULL);
e = GetTickcount();
return e-s;
}
Ich denke, es gibt mehr Dinge, die optimiert werden könnte, vielleicht einige Dosieren der anderen globalen liest zusammen, aber zuerst würde ich wirklich wie zu wissen, warum diese erste Optimierung nicht funktioniert hat.
Ich weiß nicht, über die Leistung, aber was Sie scheint dabei undefiniert Ergebnisse zu haben . Sie verwenden eine async_copy-Operation gefolgt von einer Barriere. Die Barriere wartet nicht auf das Ende der asynchronen Kopie - sie wird fortgesetzt, sobald alle Arbeitselemente diesen Punkt erreicht haben. Gemäß der Spezifikation müssen Sie die Funktion wait_group_events in Ihrem Kernel nach einer async_copy verwenden, oder die Ergebnisse sind nicht definiert. Dies ist sinnvoll, da async_copy ausgeführt wird, während der Rest des Kernels ausgeführt wird, so dass wait_group_events den Kernel dazu zwingt sicherzustellen, dass die Speicherkopie fertig ist. –