【问题标题】:horizontal sum of 8 packed 32bit floats [duplicate]8个打包的32位浮点数的水平总和[重复]
【发布时间】:2012-12-02 12:13:34
【问题描述】:

如果我有 8 个压缩的 32 位浮点数 (__m256),那么提取所有 8 个元素的水平总和的最快方法是什么?同理,如何获得水平最大值和最小值?换句话说,以下 C++ 函数的最佳实现是什么?

float sum(__m256 x);  ///< returns sum of all 8 elements
float max(__m256 x);  ///< returns the maximum of all 8 elements
float min(__m256 x);  ///< returns the minimum of all 8 elements

【问题讨论】:

  • Here is a link 到上一个关于计算压缩doubles 的水平总和的问题。您也应该能够使其适应您的 float 案例。如果您有多个要并行计算总和的__m256 元素,则效率最高。
  • @JasonR 抱歉,但这无济于事:这是一个完全不同的问题。
  • 有什么不同?您将需要使用水平添加和排列来排列要添加的术语,如另一个问题所示。您也可以对minmax 操作使用类似的结构。我知道这不是一个完整的答案(因此评论),但它应该让你开始。
  • @JasonR 好吧,是的,它并不是完全没用,但是有许多类似的问题都使用了随机和排列结合水平和垂直操作。顺便说一句,没有水平最小值/最大值,是吗?
  • 我不知道水平最小/最大操作。一种可以同时获得最小值/最大值的方法是使用寄存器内排序网络对 SIMD 寄存器中的元素进行排序。适合在__m128 上实现的算法可以在this paper 中找到;大约需要 15 条指令。在 x86 上实现 YMM 寄存器的方式可能会使对__m256 进行排序的工作变得更加困难,因为在大多数情况下您无法跨越 128 位边界。

标签: x86 sse simd avx


【解决方案1】:

在这里快速记下(因此未经测试):

float sum(__m256 x) {
    __m128 hi = _mm256_extractf128_ps(x, 1);
    __m128 lo = _mm256_extractf128_ps(x, 0);
    lo = _mm_add_ps(hi, lo);
    hi = _mm_movehl_ps(hi, lo);
    lo = _mm_add_ps(hi, lo);
    hi = _mm_shuffle_ps(lo, lo, 1);
    lo = _mm_add_ss(hi, lo);
    return _mm_cvtss_f32(lo);
}

对于最小值/最大值,将 _mm_add_ps_mm_add_ss 替换为 _mm_max_*_mm_min_*

请注意,这对于一些操作来说是很多工作; AVX 并不是真正打算有效地进行水平操作。如果您可以将这项工作批量处理多个向量,那么更有效的解决方案是可能的。

【讨论】:

    【解决方案2】:

    虽然斯蒂芬佳能的答案可能是找到水平最大值/最小值的理想选择,但我认为可以为水平总和找到更好的解决方案。

    float horizontal_add (__m256 a) {
        __m256 t1 = _mm256_hadd_ps(a,a);
        __m256 t2 = _mm256_hadd_ps(t1,t1);
        __m128 t3 = _mm256_extractf128_ps(t2,1);
        __m128 t4 = _mm_add_ss(_mm256_castps256_ps128(t2),t3);
        return _mm_cvtss_f32(t4);        
    }
    

    【讨论】:

    • 请注意,VHADDPS 在 Sandy Bridge/Ivy Bridge 上有 5 个周期的延迟,因此这实际上可能比 Stephen Canon 的实现效率低(其中所有指令通常都是 1 个周期的延迟)。
    • @PaulR,你可能是对的。但在任何情况下,水平操作都不应该在关键循环的每次迭代中进行。
    • @PaulR,我承认我过去过于强调指令的数量。现在我会查看整体延迟和吞吐量,并使用 IACA 之类的东西(并在我的应用程序中进行测试)。但无论如何,我认为我最初是从 Agner Fog 的 VCL(我学习 SSE 和 AVX 的方式)中提出了这个解决方案。如果你必须赌 Agner Fog 和 Stephen Canon 之间的解决方案,你会怎么赌?我想我会掷硬币。
    • @PaulR,恭喜获得 simd 金标签!
    • @Zboson:见agner.org/optimize/vectorclass/read.php?i=124。我对整数 hsum 做了一些改进。
    【解决方案3】:

    我尝试编写避免混合 avx 和非 avx 指令的代码,并且包含浮点数的 avx 寄存器的水平总和可以由

    avx-only 完成
    • 1x vperm2f128,
    • 2x vshufps
    • 3x vaddps,

    产生一个寄存器,其中所有条目都包含原始寄存器中所有元素的总和。

    // permute
    //  4, 5, 6, 7, 0, 1, 2, 3
    // add
    //  0+4, 1+5, 2+6, 3+7, 4+0, 5+1, 6+2, 7+3
    // shuffle
    //  1+5, 0+4, 3+7, 2+6, 5+1, 4+0, 7+3, 6+2
    // add
    //  1+5+0+4, 0+4+1+5, 3+7+2+6, 2+6+3+7, 
    //  5+1+4+0, 4+0+5+1, 7+3+6+2, 6+2+7+3
    // shuffle
    //  3+7+2+6, 2+6+3+7, 1+5+0+4, 0+4+1+5, 
    //  7+3+6+2, 6+2+7+3, 5+1+4+0, 4+0+5+1
    // add
    //  3+7+2+6+1+5+0+4, 2+6+3+7+0+4+1+5, 1+5+0+4+3+7+2+6, 0+4+1+5+2+6+3+7,
    //  7+3+6+2+5+1+4+0, 6+2+7+3+4+0+5+1, 5+1+4+0+7+3+6+2, 4+0+5+1+6+2+7+3
    
    static inline __m256 hsums(__m256 const& v)
    {
        auto x = _mm256_permute2f128_ps(v, v, 1);
        auto y = _mm256_add_ps(v, x);
        x = _mm256_shuffle_ps(y, y, _MM_SHUFFLE(2, 3, 0, 1));
        x = _mm256_add_ps(x, y);
        y = _mm256_shuffle_ps(x, x, _MM_SHUFFLE(1, 0, 3, 2));
        return _mm256_add_ps(x, y);
    }
    

    然后使用_mm256_castps256_ps128_mm_cvtss_f32 即可轻松获取值:

    static inline float hadd(__m256 const& v)
    {
        return _mm_cvtss_f32(_mm256_castps256_ps128(hsums(v)));
    }
    

    我使用__rdtscp 对其他解决方案进行了一些基本基准测试,但没有发现在我的英特尔 i5-2500k 上的平均 cpu 周期计数方面更胜一筹。

    查看我找到的Agner Instruction Tables(用于 Sandy-Bridge 处理器):

                    µops    lat.    1/tp    count
    
    this:
    
    vperm2f128      1       2       1       1
    vaddps          1       3       1       3
    vshufps         1       1       1       2
    
    sum             6       13      6       6
    
    Z boson:
    
    vhaddps         3       5       2       2
    vextractf128    1       2       1       1
    addss           1       3       1       1
    
    sum             8       15      6       4
    
    Stephen Canon:
    
    vextractf128    1       2       1       1
    addps           1       3       1       2
    movhlps         1       1       1       1
    shufps          1       1       1       1
    addss           1       3       1       1
    
    sum             8       13      6       6
    

    对我来说(由于值相当相似)没有一个是明显优越的(因为我无法预见指令数、微操作数、延迟或吞吐量是否最重要)。 编辑,注意:我假设以下存在的潜在问题不正确。 我怀疑,如果在 ymm 寄存器中有结果就足够了,我的hsums 可能很有用,因为它不需要vzeroupper 来防止状态切换损失,因此可以与其他使用不同的 avx 计算同时交错/执行在不引入某种序列点的情况下注册。

    【讨论】:

    • __m128 当您在启用 AVX 支持的情况下编译时,内在函数仍使用 AVX 3 操作数 VEX 编码版本。你说得对,ABI 需要一个 独立 版本的float hsum(__m256) 来包含 VZEROUPPER,但无论如何你总是希望它内联。在 SysV ABI 中,所有 XMM/YMM/ZMM regs 都是 call-clobbered,因此调用者必须溢出所有内容,无论函数返回 __m256 还是 float。 (而且 Windows 只有少数保留调用的 XMM regs,这只是低半部分,没有保留调用的 YMM regs。)
    • @PeterCordes:尽管内联,泄漏会发生吗?
    • 不,这是内联的主要好处之一!
    • 在计算斯蒂芬佳能的答案时,您错过了上半部分的 VEXTRACTF128。您的两个功能应该是等效的:一个车道交叉洗牌和两个车道内洗牌,以及 3 FP 添加。除了 Stephen's 在 AMD Bulldozer 系列或其他只有 128b 执行单元的 CPU 上运行得更快(所以 vaddps ymm, ymm, ymmvaddps xmm, xmm, xmm 慢)。
    • 另见my hsums answer,其中 AVX 部分使用 vextractf128、vmovshdup 和 vmovhlps,这与 Stephen 的相同,但节省了一个指令字节,因为这些 shuffle 不需要 imm8 控制操作数。
    【解决方案4】:
    union ymm {
        __m256 m256;
        struct {
            __m128 m128lo;
            __m128 m128hi;
        };
    };
    
    union ymm result = {1,2,3,4,5,6,7,8};
    __m256 a = {9,10,11,12,13,14,15,16};
    
    result.m256 = _mm256_add_ps (result.m256, a);
    result.m128lo = _mm_hadd_ps (result.m128lo, result.m128hi);
    result.m128lo = _mm_hadd_ps (result.m128lo, result.m128hi);
    result.m128lo = _mm_hadd_ps (result.m128lo, result.m128hi);
    

    【讨论】:

    • 请添加解释并详细说明您认为这将是问题的答案
    猜你喜欢
    • 2014-06-05
    • 1970-01-01
    • 2018-12-18
    • 1970-01-01
    • 2020-06-13
    • 1970-01-01
    • 2013-09-12
    • 2013-09-13
    • 2019-01-17
    相关资源
    最近更新 更多