【问题标题】:128-bit SSE counter?128 位 SSE 计数器?
【发布时间】:2012-03-10 01:48:44
【问题描述】:

我需要一个周期为 2^128 的 __m128i 变量的函数。它不需要单调递增(像计数器一样),而是每个值访问一次。

我能想到的最简单的例子实际上是一个 128 位计数器,但我发现在 SSE 中很难实现。有没有更简单/更快的解决方案?

【问题讨论】:

  • 为什么需要访问 2^128 个值?地球上没有计算机能够做到这一点。不能使用 64 位 int 吗?
  • 同意,在时钟速度约为千兆赫的处理器上,在 64 位计数器耗尽之前,您可以在大约 584 年的时间里每个周期消耗一个数字。

标签: optimization sse intrinsics avx


【解决方案1】:

这是一个单调的计数器。我不确定你是否可以称之为简单。

假设ONEZERO 总是在寄存器中,那么这应该编译为5 条指令。 (如果不使用 VEX 编码,则为 7 或 8)

inline __m128i nextc(__m128i x){
    const __m128i ONE = _mm_setr_epi32(1,0,0,0);
    const __m128i ZERO = _mm_setzero_si128();

    x = _mm_add_epi64(x,ONE);
    __m128i t = _mm_cmpeq_epi64(x,ZERO);
    t = _mm_and_si128(t,ONE);
    t = _mm_unpacklo_epi64(ZERO,t);
    x = _mm_add_epi64(x,t);

    return x;
}

测试代码(MSVC):

int main() {

    __m128i x = _mm_setr_epi32(0xfffffffa,0xffffffff,1,0);

    int c = 0;
    while (c++ < 10){
        cout << x.m128i_u64[0] << "  " << x.m128i_u64[1] << endl;
        x = nextc(x);
    }

    return 0;
}

输出:

18446744073709551610  1
18446744073709551611  1
18446744073709551612  1
18446744073709551613  1
18446744073709551614  1
18446744073709551615  1
0  2
1  2
2  2
3  2

@Norbert P 建议的稍微好一点的版本。它比我原来的解决方案节省了 1 条指令。

inline __m128i nextc(__m128i x){
    const __m128i ONE = _mm_setr_epi32(1,0,0,0);
    const __m128i ZERO = _mm_setzero_si128();

    x = _mm_add_epi64(x,ONE);
    __m128i t = _mm_cmpeq_epi64(x,ZERO);
    t = _mm_unpacklo_epi64(ZERO,t);
    x = _mm_sub_epi64(x,t);

    return x;
}

【讨论】:

  • 谢谢,你的代码比我的干净多了。我等一下看看有没有计数器以外的解决方案。
  • 问题是,这是否真的比不使用任何 SSE 的解决方案更快。我的意思是使用 2 个 64 位无符号结构和 1 个分支的明显解决方案将避免可能的 SSE 开销,并且分支将非常可预测。
  • @Voo 这可能取决于需要该值的形式。如果在通用寄存器或内存中需要它,那么add + adc 将是最快的。如果在 SSE 寄存器中需要它,那么 5 个 SSE-Int 指令可能会比任何类型的提取/插入更快。通过结构/联合将其通过内存可能会停止加载/存储缓冲区,因为您正在使用不同的字长访问相同的内存。
  • 您确实对分支有所了解。它很少会延续到上面的词。你可以尝试用_mm_testz_si128 做一些事情并在carry 上进行分支。但是您首先必须屏蔽高 64 位,所以它可能和以前一样昂贵。
  • 我觉得and 指令是不必要的,因为如果比较为真,cmpeq 指令的结果是-1(所有位设置)。只需删除t = _mm_and_si128(t,ONE); 并将x = _mm_add_epi64(x,t); 替换为x = _mm_sub_epi64(x,t);。如果我错了请忽略。
【解决方案2】:

永远不要忘记 KISS 原则。

粘贴这个(C 标准要求无符号整数环绕,因此每个值只访问一次):

__uint128_t inc(__uint128_t x) {
  return x+1;
}

进入this 产生(对于 x64):

    addq    $1, %rdi
    adcq    $0, %rsi
    movq    %rdi, %rax
    movq    %rsi, %rdx
    ret

够简单/够快吗?如果你内联它,你可能只需要addq/adcq(x64 ABI 需要movqs 和ret:如果你内联函数,它们不是必填)


要解决 Voo 关于 MSVC 的糟糕之处的评论,您可以使用以下内容:

inline void inc(unsigned long long *x, unsigned long long *y) {
  if (!++*x) ++*y; // yay for obfuscation!
}

我附近没有安装 MSVC,因此无法对其进行测试,但它应该产生类似于我上面发布的内容。那么,如果你真的需要一个 __m128i,你应该可以cast 两半。

【讨论】:

  • 这个解决方案的问题是从 SSE 寄存器加载和存储会花费很多时间。
  • 代码是否有效很大程度上取决于编译器。很确定它不在 MSVC 下 - __m128 只是较小原语的联合结构。
  • @Voo 查看我的更新答案。顺便说一句,您为什么要首先寻找 SSE 解决方案?
  • 它用于 SSE 代码的其他部分(键入一些计算。)但除了非 SSE 之外,您的解决方案很棒!
猜你喜欢
  • 2011-05-30
  • 2017-10-28
  • 2017-06-08
  • 1970-01-01
  • 1970-01-01
  • 2012-08-25
  • 1970-01-01
  • 2013-09-23
  • 1970-01-01
相关资源
最近更新 更多