_mm_sad_epu8比_mm_sad_pu8更快

5
在基准测试中,128位内在函数的执行速度比64位内在函数更快?
_mm_sad_epu8(__m128i, __m128i) //Clocks: 0.0300
_mm_sad_pu8(__m64, __m64)      //Clocks: 0.0491

据我了解,英特尔参考手册指出,在MMX寄存器上,(PSADBW)的延迟为5,吞吐量为1,但未说明MM寄存器的性能表现。
它们的速度不应该是相同的吗?对于使用128位参数的内在函数,这种情况是普遍的吗?

你是如何衡量执行时间的? - Martin Zabel
clock()/CLOCKS_PER_SEC - sigvardsen
2
好的,那么:基准循环运行了几秒钟吗?处理器是否达到了相同的超频频率?实际上是哪款处理器呢? - Martin Zabel
我已经尝试了不同的循环迭代,长达几秒钟。我还尝试重新排列语句,以消除潜在的Turbo CPU功能。 - sigvardsen
你说得完全正确,我一定是误读了指令集参考。我已经编辑了我的问题。我仍然觉得它在mm寄存器上运行得更慢有点奇怪。 - sigvardsen
显示剩余2条评论
1个回答

4
我的测量程序(见下文)显示,在Core-i5 3450(常春藤桥)上,_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 控制台应用程序,并使用标准的“发布”设置编译了程序。这是 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有足够的(流水线)端口同时执行此操作。生成的代码仅使用xmm1和xmm0寄存器,因此它依赖于处理器的寄存器重命名。由于地址算术的变化,代码长度从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版本中增加了元素数量(请参见主函数)。


感谢您的辛勤工作。这段代码与我创建的基准程序几乎相同。但是,我在Xcode中编译它,可能会因为不同的编译设置而产生不同的结果。 - sigvardsen
那并不完全适合寄存器,所以每个psadbw都需要进行加载和存储。将其编写为_mm_sad_epu8(zero,input[i+1]);(参数顺序相反)确实会说服gcc将加载折叠到内存参数中,就像clang一样总是这样做(当针对AVX时,3操作数非破坏性操作允许在不破坏零向量寄存器的情况下工作)。无论如何,这可能仍然会使您的IvB上的端口0饱和。也许MMX版本的较小缓存占用量使得差异微不足道:如果被驱逐,需要重新填充的缓存行更少? - Peter Cordes
@PeterCordes 我在我的回答中添加了有关生成代码的更多细节。由于我在 MMX 版本中将元素数量加倍(请参见 main),因此两个版本的缓存占用量相同。折叠在我的编译器中无法工作。您使用的是 64 位编译器。我目前只有一个 32 位编译器。 :-( - Martin Zabel
哦,我明白了,你实际上是在一个更大的缓冲区上循环。我原本以为代码只会在一个包含16个元素的数组上循环,假设你希望它发出仅由 psadbw 指令组成的循环,而没有任何可能成为瓶颈的加载/存储操作。所以我没有仔细阅读,没看到你完全不是这样做的! - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接