我正在努力改善我的功能。 Profiler指向内循环的代码。我可以改进该代码的性能,可能使用SSE内在函数吗?
void ConvertImageFrom_R16_FLOAT_To_R32_FLOAT(char* buffer, void* convertedData, DWORD width, DWORD height, UINT rowPitch)
{
struct SINGLE_FLOAT
{
union {
struct {
unsigned __int32 R_m : 23;
unsigned __int32 R_e : 8;
unsigned __int32 R_s : 1;
};
struct {
float r;
};
};
};
C_ASSERT(sizeof(SINGLE_FLOAT) == 4); // 4 bytes
struct HALF_FLOAT
{
unsigned __int16 R_m : 10;
unsigned __int16 R_e : 5;
unsigned __int16 R_s : 1;
};
C_ASSERT(sizeof(HALF_FLOAT) == 2);
SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
for(DWORD j = 0; j< height; j++)
{
HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
for(DWORD i = 0; i< width; i++)
{
d->R_s = s->R_s;
d->R_e = s->R_e - 15 + 127;
d->R_m = s->R_m << (23-10);
d++;
s++;
}
}
}
更新:
拆卸
; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01
TITLE Utils.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
PUBLIC ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
; Function compile flags: /Ogtp
; COMDAT ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z
_TEXT SEGMENT
_buffer$ = 8 ; size = 4
tv83 = 12 ; size = 4
_convertedData$ = 12 ; size = 4
_width$ = 16 ; size = 4
_height$ = 20 ; size = 4
_rowPitch$ = 24 ; size = 4
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z PROC ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT, COMDAT
; 323 : {
push ebp
mov ebp, esp
; 343 : for(DWORD j = 0; j< height; j++)
mov eax, DWORD PTR _height$[ebp]
push esi
mov esi, DWORD PTR _convertedData$[ebp]
test eax, eax
je SHORT $LN4@ConvertIma
; 324 : union SINGLE_FLOAT {
; 325 : struct {
; 326 : unsigned __int32 R_m : 23;
; 327 : unsigned __int32 R_e : 8;
; 328 : unsigned __int32 R_s : 1;
; 329 : };
; 330 : struct {
; 331 : float r;
; 332 : };
; 333 : };
; 334 : C_ASSERT(sizeof(SINGLE_FLOAT) == 4);
; 335 : struct HALF_FLOAT
; 336 : {
; 337 : unsigned __int16 R_m : 10;
; 338 : unsigned __int16 R_e : 5;
; 339 : unsigned __int16 R_s : 1;
; 340 : };
; 341 : C_ASSERT(sizeof(HALF_FLOAT) == 2);
; 342 : SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
push ebx
mov ebx, DWORD PTR _buffer$[ebp]
push edi
mov DWORD PTR tv83[ebp], eax
$LL13@ConvertIma:
; 344 : {
; 345 : HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
; 346 : for(DWORD i = 0; i< width; i++)
mov edi, DWORD PTR _width$[ebp]
mov edx, ebx
test edi, edi
je SHORT $LN5@ConvertIma
npad 1
$LL3@ConvertIma:
; 347 : {
; 348 : d->R_s = s->R_s;
movzx ecx, WORD PTR [edx]
movzx eax, WORD PTR [edx]
shl ecx, 16 ; 00000010H
xor ecx, DWORD PTR [esi]
shl eax, 16 ; 00000010H
and ecx, 2147483647 ; 7fffffffH
xor ecx, eax
mov DWORD PTR [esi], ecx
; 349 : d->R_e = s->R_e - 15 + 127;
movzx eax, WORD PTR [edx]
shr eax, 10 ; 0000000aH
and eax, 31 ; 0000001fH
add eax, 112 ; 00000070H
shl eax, 23 ; 00000017H
xor eax, ecx
and eax, 2139095040 ; 7f800000H
xor eax, ecx
mov DWORD PTR [esi], eax
; 350 : d->R_m = s->R_m << (23-10);
movzx ecx, WORD PTR [edx]
and ecx, 1023 ; 000003ffH
shl ecx, 13 ; 0000000dH
and eax, -8388608 ; ff800000H
or ecx, eax
mov DWORD PTR [esi], ecx
; 351 : d++;
add esi, 4
; 352 : s++;
add edx, 2
dec edi
jne SHORT $LL3@ConvertIma
$LN5@ConvertIma:
; 343 : for(DWORD j = 0; j< height; j++)
add ebx, DWORD PTR _rowPitch$[ebp]
dec DWORD PTR tv83[ebp]
jne SHORT $LL13@ConvertIma
pop edi
pop ebx
$LN4@ConvertIma:
pop esi
; 353 : }
; 354 : }
; 355 : }
pop ebp
ret 0
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ENDP ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
_TEXT ENDS
答案 0 :(得分:3)
x86 F16C instruction-set extension增加了硬件支持,用于将单精度浮点向量转换为半精度浮点数的向量。
The format is the same IEEE 754 half-precision binary16 that you describe。我没有检查字节顺序是否与结构相同,但如果需要,可以很容易地修复(使用pshufb
)。
从Intel IvyBridge和AMD Piledriver开始支持F16C。 (并且有自己的CPUID功能位,您的代码应该检查它,否则回退到SIMD整数移位和随机播放)。
VCVTPS2PH的内在函数是:
__m128i _mm_cvtps_ph ( __m128 m1, const int imm);
__m128i _mm256_cvtps_ph(__m256 m1, const int imm);
立即数字是舍入控制。编译器可以将它直接用作转换存储器(与大多数可以选择使用内存操作数的指令不同,它是源操作数,可以是内存而不是寄存器。)
VCVTPH2PS是另一种方式,就像大多数其他SSE指令一样(可以在寄存器之间使用或作为负载使用)。
__m128 _mm_cvtph_ps ( __m128i m1);
__m256 _mm256_cvtph_ps ( __m128i m1)
F16C非常高效,您可能需要考虑将图像保留为半精度格式,并在每次需要数据向量时动态转换。这非常适合您的缓存占用空间。
答案 1 :(得分:2)
当然,访问内存中的位域可能非常棘手,具体取决于架构。
如果要建立float和32位整数的并集,并使用局部变量执行所有分解和组合,则可能会获得更好的性能。这样生成的代码就可以仅使用处理器寄存器来执行整个操作。
答案 2 :(得分:2)
以下是一些想法:
const register
个变量。有些处理器不喜欢从内存中获取常量;它很尴尬,可能需要很多指令周期。
重复循环中的语句,并增加增量 处理器更喜欢连续的指令;跳跃和分支愤怒他们。
在循环中使用更多变量,并将它们声明为volatile
,以便编译器不优化它们:
SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
SINGLE_FLOAT* d1 = d + 1;
SINGLE_FLOAT* d2 = d + 2;
SINGLE_FLOAT* d3 = d + 3;
for(DWORD j = 0; j< height; j++)
{
HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
HALF_FLOAT* s1 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 1));
HALF_FLOAT* s2 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 2));
HALF_FLOAT* s3 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 3));
for(DWORD i = 0; i< width; i += 4)
{
d->R_s = s->R_s;
d->R_e = s->R_e - 15 + 127;
d->R_m = s->R_m << (23-10);
d1->R_s = s1->R_s;
d1->R_e = s1->R_e - 15 + 127;
d1->R_m = s1->R_m << (23-10);
d2->R_s = s2->R_s;
d2->R_e = s2->R_e - 15 + 127;
d2->R_m = s2->R_m << (23-10);
d3->R_s = s3->R_s;
d3->R_e = s3->R_e - 15 + 127;
d3->R_m = s3->R_m << (23-10);
d += 4;
d1 += 4;
d2 += 4;
d3 += 4;
s += 4;
s1 += 4;
s2 += 4;
s3 += 4;
}
}
答案 3 :(得分:1)
循环彼此独立,因此您可以轻松地并行化此代码,无论是使用SIMD还是OpenMP,简单版本将图像的上半部分和下半部分分成两个并行运行的线程。 / p>
答案 4 :(得分:1)
SSE Intrinsics似乎是一个很好的主意。在你走这条路之前,你应该
查看编译器生成的汇编代码,(有可能进行优化吗?)
在编译器文档中搜索如何自动生成SSE代码,
搜索软件库的文档(或16位浮点类型的来源),以便批量转换此类型的函数。 (转换为64位浮点也可能会有所帮助。)您很可能不是第一个遇到此问题的人!
如果一切都失败了,那就试着运气一些SSE内在函数吧。为了得到一些想法,here是一些SSE代码,可以从32位浮点转换为16位浮点数。 (你想反过来)
除了SSE,你还应该考虑多线程并将任务卸载到GPU。
答案 5 :(得分:1)
您将数据作为二维数组处理。如果你考虑它是如何在内存中布局的,你可以将它作为一个单独的数组处理,你可以通过一个循环而不是嵌套循环来节省一些开销。
我还要编译成汇编代码并确保编译器优化工作,并且不会重新计算(15 + 127)数百次。
答案 6 :(得分:1)
您应该能够将此减少为使用即将到来的CVT16 instruction set的芯片上的单个指令。根据维基百科的文章:
The CVT16 instructions allow conversion of floating point vectors between single precision and half precision.
答案 7 :(得分:0)
我不知道SSE内在函数,但看到你的内循环的反汇编会很有趣。一个老派的方式(可能没什么用,但很容易尝试)将通过做两个内部循环来减少迭代次数:一个做N次(比如32次)重复处理(循环次数为width / N)然后用一个来完成余数(宽度%N的循环计数)...用在第一个循环外计算的div和模数来避免重新计算它们。如果这听起来很明显,请道歉!
答案 8 :(得分:0)
该功能只做了一些小事。通过优化来节省很多时间是很困难的,但正如有人已经说过的,并行化有希望。
检查您获得的缓存未命中数。如果数据是分页输入和输出,您可以通过在排序中应用更多智能来最小化缓存交换来加快速度。
还要考虑宏优化。数据计算中是否存在可以避免的冗余(例如,缓存旧结果而不是在需要时重新计算它们)?你真的需要转换整个数据集还是只需转换你需要的位数?我不知道你的申请,所以我只是在这里疯狂地猜测,但可能有这种优化的余地。
答案 9 :(得分:0)
我怀疑这个操作在内存访问上已经存在瓶颈,并且使其更有效(例如,使用SSE)不会使它更快地执行。然而,这只是一种怀疑。
假设x86 / x64,其他尝试可能是:
d++
和s++
,而是在每次迭代时使用d[i]
和s[i]
。 (当然然后在每条扫描线之后碰撞d
。)由于d
的元素是4字节而s
2的元素,因此可以将此操作折叠到地址计算中。 (不幸的是,我不能保证这会使执行更有效率。)width
的每一行向下计数到零。这会阻止编译器每次都获取width
。对于x86来说可能更重要,因为它的寄存器很少。 (如果CPU喜欢我的“d[i]
和s[i]
”建议,您可以将宽度签名,从width-1
开始计数,然后向后走。)尝试这些比转换为SSE更快,并且希望能使它受内存限制,如果还没有,那么你可以放弃。
最后,如果输出位于写入组合存储器中(例如,它是纹理或顶点缓冲区或通过AGP或PCI Express访问的东西,或PC现在拥有的任何东西)那么这很可能导致性能不佳,取决于编译器为内循环生成的代码。因此,如果是这种情况,您可以获得更好的结果,将每个扫描线转换为本地缓冲区,然后使用memcpy
将其复制到其最终目标。