_mm_sad_epu8 比 _mm_sad_pu8 快
_mm_sad_epu8 faster than _mm_sad_pu8
在基准测试中,128 位内部函数比 64 位内部函数执行得更快?
_mm_sad_epu8(__m128i, __m128i) //Clocks: 0.0300
_mm_sad_pu8(__m64, __m64) //Clocks: 0.0491
据我了解,英特尔参考手册指出 (PSADBW)
在 mmx 寄存器上的延迟为 5,吞吐量为 1,但没有说明 mm 寄存器的性能。
它们不应该同样快吗,这对于采用 128 位参数的内部函数来说是普遍的吗?
我的测量程序(见下文)显示在 Core-i5 3450(Ivy Bridge)上 _mm_sad_epu8
的性能等于 _mm_sad_pu8
。后者甚至更快。
我程序的输出是:
warmup: 1.918 sec total
measure_mm_sad_epu8: 1.904 sec total, 0.372 nsec per operation
measure_mm_sad_pu8: 1.872 sec total, 0.366 nsec per operation
我的处理器的 Turbo 时钟频率是 3.5 GHz(单线程),根据 Intrinsics Guide,_mm_sad_epu8
的吞吐量应该是 1 个时钟周期。因此,每个操作至少需要 0.286 纳秒。所以我的测量程序达到了最大性能的77%左右。
我使用 Visual Studio C++ 2010 Express 并创建了一个新的 Win32 控制台应用程序。该程序已使用标准 "Release" 设置进行编译。这是cpp文件的代码:
#include "stdafx.h"
#include <cassert>
#include <ctime>
#include <iostream>
#include <iomanip>
extern "C" {
#include <emmintrin.h>
}
float measure_mm_sad_epu8(int n, int repeat) {
assert(n % 16 == 0);
// Didn't get an aligned "new" to work :-(
__m128i *input = (__m128i *) _aligned_malloc(n * sizeof *input, 16);
__m128i *output = (__m128i *) _aligned_malloc(n * sizeof *output, 16);
if(!input || !output) exit(1);
__m128i zero = _mm_setzero_si128();
for(int i=0; i < n; i++) {
input[i].m128i_i64[0] = 0x0123456789abcdef;
input[i].m128i_i64[1] = 0xfedcba9876543210;
}
clock_t startTime = clock();
for(int r = 0; r < repeat; r++) {
for(int i = 0; i < n; i+=16) { // loop unrolled
output[i ] = _mm_sad_epu8(input[i ], zero);
output[i+1] = _mm_sad_epu8(input[i+1], zero);
output[i+2] = _mm_sad_epu8(input[i+2], zero);
output[i+3] = _mm_sad_epu8(input[i+3], zero);
output[i+4] = _mm_sad_epu8(input[i+4], zero);
output[i+5] = _mm_sad_epu8(input[i+5], zero);
output[i+6] = _mm_sad_epu8(input[i+6], zero);
output[i+7] = _mm_sad_epu8(input[i+7], zero);
output[i+8] = _mm_sad_epu8(input[i+8], zero);
output[i+9] = _mm_sad_epu8(input[i+9], zero);
output[i+10] = _mm_sad_epu8(input[i+10], zero);
output[i+11] = _mm_sad_epu8(input[i+11], zero);
output[i+12] = _mm_sad_epu8(input[i+12], zero);
output[i+13] = _mm_sad_epu8(input[i+13], zero);
output[i+14] = _mm_sad_epu8(input[i+14], zero);
output[i+15] = _mm_sad_epu8(input[i+15], zero);
}
}
_mm_empty();
clock_t endTime = clock();
_aligned_free(input);
_aligned_free(output);
return (endTime-startTime)/(float)CLOCKS_PER_SEC;
}
float measure_mm_sad_pu8(int n, int repeat) {
assert(n % 16 == 0);
// Didn't get an aligned "new" to work :-(
__m64 *input = (__m64 *) _aligned_malloc(n * sizeof *input, 16);
__m64 *output = (__m64 *) _aligned_malloc(n * sizeof *output, 16);
if(!input || !output) exit(1);
__m64 zero = _mm_setzero_si64();
for(int i=0; i < n; i+=2) {
input[i ].m64_i64 = 0x0123456789abcdef;
input[i+1].m64_i64 = 0xfedcba9876543210;
}
clock_t startTime = clock();
for(int r = 0; r < repeat; r++) {
for(int i = 0; i < n; i+=16) { // loop unrolled
output[i ] = _mm_sad_pu8(input[i ], zero);
output[i+1] = _mm_sad_pu8(input[i+1], zero);
output[i+2] = _mm_sad_pu8(input[i+2], zero);
output[i+3] = _mm_sad_pu8(input[i+3], zero);
output[i+4] = _mm_sad_pu8(input[i+4], zero);
output[i+5] = _mm_sad_pu8(input[i+5], zero);
output[i+6] = _mm_sad_pu8(input[i+6], zero);
output[i+6] = _mm_sad_pu8(input[i+7], zero);
output[i+7] = _mm_sad_pu8(input[i+8], zero);
output[i+8] = _mm_sad_pu8(input[i+9], zero);
output[i+10] = _mm_sad_pu8(input[i+10], zero);
output[i+11] = _mm_sad_pu8(input[i+11], zero);
output[i+12] = _mm_sad_pu8(input[i+12], zero);
output[i+13] = _mm_sad_pu8(input[i+13], zero);
output[i+14] = _mm_sad_pu8(input[i+14], zero);
output[i+15] = _mm_sad_pu8(input[i+15], zero);
}
}
_mm_empty();
clock_t endTime = clock();
_aligned_free(input);
_aligned_free(output);
return (endTime-startTime)/(float)CLOCKS_PER_SEC;
}
int _tmain(int argc, _TCHAR* argv[])
{
int n = 256, repeat = 20000000;
float time;
std::cout << std::setprecision(3) << std::fixed;
time = measure_mm_sad_epu8(n,repeat);
std::cout << "warmup: " << time << " sec total" << std::endl;
time = measure_mm_sad_epu8(n,repeat);
std::cout << "measure_mm_sad_epu8: " << time << " sec total, " << time/n/repeat*1e9 << " nsec per operation" << std::endl;
n*=2; // same memory footprint
repeat/=2; // but with same amount of calculations
time = measure_mm_sad_pu8(n,repeat);
std::cout << "measure_mm_sad_pu8: " << time << " sec total, " << time/n/repeat*1e9 << " nsec per operation" << std::endl;
return 0;
}
这是未修改的 "stdafx.h":
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
编辑:对于每个操作output[i] = _mm_sad_epu8(input[i], zero);
在展开的循环中,编译器生成一个矢量加载、一个 psadbw
和一个像这样的矢量存储(只是使用不同的指针算法):
013410D0 movdqa xmm1,xmmword ptr [ecx-30h]
013410D5 psadbw xmm1,xmm0
013410D9 movdqa xmmword ptr [eax-10h],xmm1
013410DE ...
IvyBridge 有足够的(管道)端口在 "same" 时间执行此操作。
生成的代码只使用了xmm1
和xmm0
寄存器,因此它依赖于处理器的寄存器重命名。 EDIT2:由于地址算法的变化,代码长度从13到20字节不等。因此,代码可能会遇到解码器瓶颈,因为 Ivy Bridge 每个时钟周期只能解码 16 个字节(最多 4 条指令)。另一方面,它有一个循环缓存来处理这个问题。
MMX版本的生成代码几乎是一样的:
013412D4 movq mm1,mmword ptr [ecx-18h]
013412D8 psadbw mm1,mm0
013412DB movq mmword ptr [eax-8],mm1
013412DF ...
两个版本的内存占用都是 2*4 KiB,因为我在 MMX 版本中将元素数量翻了一番(参见 main)。
在基准测试中,128 位内部函数比 64 位内部函数执行得更快?
_mm_sad_epu8(__m128i, __m128i) //Clocks: 0.0300
_mm_sad_pu8(__m64, __m64) //Clocks: 0.0491
据我了解,英特尔参考手册指出 (PSADBW)
在 mmx 寄存器上的延迟为 5,吞吐量为 1,但没有说明 mm 寄存器的性能。
它们不应该同样快吗,这对于采用 128 位参数的内部函数来说是普遍的吗?
我的测量程序(见下文)显示在 Core-i5 3450(Ivy Bridge)上 _mm_sad_epu8
的性能等于 _mm_sad_pu8
。后者甚至更快。
我程序的输出是:
warmup: 1.918 sec total
measure_mm_sad_epu8: 1.904 sec total, 0.372 nsec per operation
measure_mm_sad_pu8: 1.872 sec total, 0.366 nsec per operation
我的处理器的 Turbo 时钟频率是 3.5 GHz(单线程),根据 Intrinsics Guide,_mm_sad_epu8
的吞吐量应该是 1 个时钟周期。因此,每个操作至少需要 0.286 纳秒。所以我的测量程序达到了最大性能的77%左右。
我使用 Visual Studio C++ 2010 Express 并创建了一个新的 Win32 控制台应用程序。该程序已使用标准 "Release" 设置进行编译。这是cpp文件的代码:
#include "stdafx.h"
#include <cassert>
#include <ctime>
#include <iostream>
#include <iomanip>
extern "C" {
#include <emmintrin.h>
}
float measure_mm_sad_epu8(int n, int repeat) {
assert(n % 16 == 0);
// Didn't get an aligned "new" to work :-(
__m128i *input = (__m128i *) _aligned_malloc(n * sizeof *input, 16);
__m128i *output = (__m128i *) _aligned_malloc(n * sizeof *output, 16);
if(!input || !output) exit(1);
__m128i zero = _mm_setzero_si128();
for(int i=0; i < n; i++) {
input[i].m128i_i64[0] = 0x0123456789abcdef;
input[i].m128i_i64[1] = 0xfedcba9876543210;
}
clock_t startTime = clock();
for(int r = 0; r < repeat; r++) {
for(int i = 0; i < n; i+=16) { // loop unrolled
output[i ] = _mm_sad_epu8(input[i ], zero);
output[i+1] = _mm_sad_epu8(input[i+1], zero);
output[i+2] = _mm_sad_epu8(input[i+2], zero);
output[i+3] = _mm_sad_epu8(input[i+3], zero);
output[i+4] = _mm_sad_epu8(input[i+4], zero);
output[i+5] = _mm_sad_epu8(input[i+5], zero);
output[i+6] = _mm_sad_epu8(input[i+6], zero);
output[i+7] = _mm_sad_epu8(input[i+7], zero);
output[i+8] = _mm_sad_epu8(input[i+8], zero);
output[i+9] = _mm_sad_epu8(input[i+9], zero);
output[i+10] = _mm_sad_epu8(input[i+10], zero);
output[i+11] = _mm_sad_epu8(input[i+11], zero);
output[i+12] = _mm_sad_epu8(input[i+12], zero);
output[i+13] = _mm_sad_epu8(input[i+13], zero);
output[i+14] = _mm_sad_epu8(input[i+14], zero);
output[i+15] = _mm_sad_epu8(input[i+15], zero);
}
}
_mm_empty();
clock_t endTime = clock();
_aligned_free(input);
_aligned_free(output);
return (endTime-startTime)/(float)CLOCKS_PER_SEC;
}
float measure_mm_sad_pu8(int n, int repeat) {
assert(n % 16 == 0);
// Didn't get an aligned "new" to work :-(
__m64 *input = (__m64 *) _aligned_malloc(n * sizeof *input, 16);
__m64 *output = (__m64 *) _aligned_malloc(n * sizeof *output, 16);
if(!input || !output) exit(1);
__m64 zero = _mm_setzero_si64();
for(int i=0; i < n; i+=2) {
input[i ].m64_i64 = 0x0123456789abcdef;
input[i+1].m64_i64 = 0xfedcba9876543210;
}
clock_t startTime = clock();
for(int r = 0; r < repeat; r++) {
for(int i = 0; i < n; i+=16) { // loop unrolled
output[i ] = _mm_sad_pu8(input[i ], zero);
output[i+1] = _mm_sad_pu8(input[i+1], zero);
output[i+2] = _mm_sad_pu8(input[i+2], zero);
output[i+3] = _mm_sad_pu8(input[i+3], zero);
output[i+4] = _mm_sad_pu8(input[i+4], zero);
output[i+5] = _mm_sad_pu8(input[i+5], zero);
output[i+6] = _mm_sad_pu8(input[i+6], zero);
output[i+6] = _mm_sad_pu8(input[i+7], zero);
output[i+7] = _mm_sad_pu8(input[i+8], zero);
output[i+8] = _mm_sad_pu8(input[i+9], zero);
output[i+10] = _mm_sad_pu8(input[i+10], zero);
output[i+11] = _mm_sad_pu8(input[i+11], zero);
output[i+12] = _mm_sad_pu8(input[i+12], zero);
output[i+13] = _mm_sad_pu8(input[i+13], zero);
output[i+14] = _mm_sad_pu8(input[i+14], zero);
output[i+15] = _mm_sad_pu8(input[i+15], zero);
}
}
_mm_empty();
clock_t endTime = clock();
_aligned_free(input);
_aligned_free(output);
return (endTime-startTime)/(float)CLOCKS_PER_SEC;
}
int _tmain(int argc, _TCHAR* argv[])
{
int n = 256, repeat = 20000000;
float time;
std::cout << std::setprecision(3) << std::fixed;
time = measure_mm_sad_epu8(n,repeat);
std::cout << "warmup: " << time << " sec total" << std::endl;
time = measure_mm_sad_epu8(n,repeat);
std::cout << "measure_mm_sad_epu8: " << time << " sec total, " << time/n/repeat*1e9 << " nsec per operation" << std::endl;
n*=2; // same memory footprint
repeat/=2; // but with same amount of calculations
time = measure_mm_sad_pu8(n,repeat);
std::cout << "measure_mm_sad_pu8: " << time << " sec total, " << time/n/repeat*1e9 << " nsec per operation" << std::endl;
return 0;
}
这是未修改的 "stdafx.h":
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
编辑:对于每个操作output[i] = _mm_sad_epu8(input[i], zero);
在展开的循环中,编译器生成一个矢量加载、一个 psadbw
和一个像这样的矢量存储(只是使用不同的指针算法):
013410D0 movdqa xmm1,xmmword ptr [ecx-30h]
013410D5 psadbw xmm1,xmm0
013410D9 movdqa xmmword ptr [eax-10h],xmm1
013410DE ...
IvyBridge 有足够的(管道)端口在 "same" 时间执行此操作。
生成的代码只使用了xmm1
和xmm0
寄存器,因此它依赖于处理器的寄存器重命名。 EDIT2:由于地址算法的变化,代码长度从13到20字节不等。因此,代码可能会遇到解码器瓶颈,因为 Ivy Bridge 每个时钟周期只能解码 16 个字节(最多 4 条指令)。另一方面,它有一个循环缓存来处理这个问题。
MMX版本的生成代码几乎是一样的:
013412D4 movq mm1,mmword ptr [ecx-18h]
013412D8 psadbw mm1,mm0
013412DB movq mmword ptr [eax-8],mm1
013412DF ...
两个版本的内存占用都是 2*4 KiB,因为我在 MMX 版本中将元素数量翻了一番(参见 main)。