为什么这个比memcmp慢

3

我正在尝试比较两行像素。

pixel被定义为一个包含4个float值(RGBA)的struct

我没有使用memcmp的原因是因为我需要返回第一个不同像素的位置,而memcmp不能做到这一点。

我的第一种实现使用了SSE内置函数,并且比memcmp慢了约30%:

inline int PixelMemCmp(const Pixel* a, const Pixel* b, int count)
{
    for (int i = 0; i < count; i++)
    {
        __m128 x = _mm_load_ps((float*)(a + i));
        __m128 y = _mm_load_ps((float*)(b + i));
        __m128 cmp = _mm_cmpeq_ps(x, y);
        if (_mm_movemask_ps(cmp) != 15) return i;
    }
    return -1;
}

我发现将值视为整数而不是浮点数可以加快速度,现在只比memcmp慢约20%。

inline int PixelMemCmp(const Pixel* a, const Pixel* b, int count)
{
    for (int i = 0; i < count; i++)
    {
        __m128i x = _mm_load_si128((__m128i*)(a + i));
        __m128i y = _mm_load_si128((__m128i*)(b + i));
        __m128i cmp = _mm_cmpeq_epi32(x, y);
        if (_mm_movemask_epi8(cmp) != 0xffff) return i; 
    }
    return -1;
}

根据我在其他问题上的阅读,微软对memcmp的实现也使用了SSE。我的问题是微软还有哪些技巧,而我不知道呢?尽管它进行逐字节比较,但为什么它仍然更快?
对齐是否是一个问题?如果pixel包含4个浮点数,那么像素数组已经分配在16字节边界上了吗?
我正在使用/o2和所有优化标志编译。

2
不,除非你自己注意,否则不能保证它将对齐在16字节上。 - interjay
是的,对齐是一个问题。您使用的编译选项也很重要。您还应该显示生成的汇编代码。也许编译器在您的代码上缺少循环展开或其他优化。 - Marc Glisse
3个回答

3
你可能想要查看这个memcmp SSE实现,特别是__sse_memcmp函数,它从一些安全检查开始,然后检查指针是否对齐:
aligned_a = ( (unsigned long)a & (sizeof(__m128i)-1) );
aligned_b = ( (unsigned long)b & (sizeof(__m128i)-1) );

如果它们不是对齐的,它会逐个比较指针的字节,直到达到对齐地址的开头:

 while( len && ( (unsigned long) a & ( sizeof(__m128i)-1) ) )
{
   if(*a++ != *b++) return -1;
   --len;
}

然后使用类似于您的代码的SSE指令比较剩余内存:

 if(!len) return 0;
while( len && !(len & 7 ) )
{
__m128i x = _mm_load_si128( (__m128i*)&a[i]);
__m128i y = _mm_load_si128( (__m128i*)&b[i]);
....

谢谢,那段代码在某种程度上很有用,但我正在处理可以预先对齐的数据,因此所有的健全性检查和尾部逻辑对我的情况不相关。 - Rotem
@Rotem 如果你可以对齐数据,那么你就不需要它了,你测试过带有对齐的代码吗? - iabdalkader
是的,结果完全相同(通过 _aligned_malloc 对齐)。我不确定如何解释这个事实。 - Rotem

3
我使用SSE(和MMX/3DNow!)编写了strcmp/memcmp优化,第一步是确保数组尽可能对齐 - 你可能会发现你需要逐个处理第一个和/或最后一个字节。
如果你可以在循环之前对数据进行对齐[如果你的代码执行分配],那就太理想了。
第二部分是展开循环,这样你就不会得到太多的“如果循环不在结尾,则跳回循环开始” - 假设循环非常长。
你可能会发现,在执行“我们现在离开吗”的条件之前,预加载输入的下一个数据也有帮助。
编辑:最后一段可能需要一个例子。此代码假定至少有两个展开的循环。
 __m128i x = _mm_load_si128((__m128i*)(a));
 __m128i y = _mm_load_si128((__m128i*)(b));

 for(int i = 0; i < count; i+=2)
 {
    __m128i cmp = _mm_cmpeq_epi32(x, y);

    __m128i x1 = _mm_load_si128((__m128i*)(a + i + 1));
    __m128i y1 = _mm_load_si128((__m128i*)(b + i + 1));

    if (_mm_movemask_epi8(cmp) != 0xffff) return i; 
    cmp = _mm_cmpeq_epi32(x1, y1);
    __m128i x = _mm_load_si128((__m128i*)(a + i + 2));
    __m128i y = _mm_load_si128((__m128i*)(b + i + 2));
    if (_mm_movemask_epi8(cmp) != 0xffff) return i + 1; 
}

大致就像那样。

1
展开循环真的很有用!我将循环展开了4倍,从比“memcmp”慢20%的速度提高到比“memcmp”快20%的速度。出于某种原因,对齐似乎没有任何影响(“malloc”与“_aligned_malloc(16)”)。您能否解释一下最后一段是什么意思?我没有理解您的意思。 - Rotem
1
如果您的输入数组尚未对齐到16字节,那么您将会遇到崩溃问题,因为您正在使用对齐版本的加载函数(例如 _mm_load_si128()_mm_loadu_si128())。如果您想要针对潜在的非对齐输入具有健壮性,那么您可以使用非对齐的加载函数,但即使数组已经对齐,性能也会稍微受到影响。 - Jason R

0

我无法直接帮助你,因为我正在使用Mac,但有一种简单的方法可以弄清楚发生了什么:

您只需在调试模式下进入memcpy并切换到反汇编视图。由于memcpy是一个简单的小函数,您将轻松地找出所有实现技巧。


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