修改函数以使用 SSE 内在函数

Posted

技术标签:

【中文标题】修改函数以使用 SSE 内在函数【英文标题】:Modifying a function to use SSE intrinsics 【发布时间】:2015-01-30 14:04:46 【问题描述】:

我正在尝试计算激进的近似值:sqrt(i + sqrt(i + sqrt(i + ...))) 使用 SSE 以从矢量化中获得加速(我还读到 SIMD 平方根函数的运行速度比先天 FPU 平方快大约 4.7 倍-根函数)。但是,我在矢量化版本中获得相同的功能时遇到问题;我得到的值不正确,我不确定

我原来的功能是这样的:

template <typename T>
T CalculateRadical( T tValue, T tEps = std::numeric_limits<T>::epsilon() )

    static std::unordered_map<T,T> setResults;

    auto it = setResults.find( tValue );
    if( it != setResults.end() )
    
        return it->second;
    

    T tPrev = std::sqrt(tValue + std::sqrt(tValue)), tCurr = std::sqrt(tValue + tPrev);

    // Keep iterating until we get convergence:
    while( std::abs( tPrev - tCurr ) > tEps )
    
        tPrev = tCurr;
        tCurr = std::sqrt(tValue + tPrev);
    

    setResults.insert( std::make_pair( tValue, tCurr ) );
    return tCurr;

我写的 SIMD 等价物(当这个模板函数用T = float 实例化并给出tEps = 0.0005f)是:

// SSE intrinsics hard-coded function:
__m128 CalculateRadicals( __m128 values )

    static std::unordered_map<float, __m128> setResults;

    // Store our epsilon as a vector for quick comparison:
    __declspec(align(16)) float flEps[4] =  0.0005f, 0.0005f, 0.0005f, 0.0005f ;
    __m128 eps = _mm_load_ps( flEps );

    union U 
        __m128 vec;
        float flArray[4];
    ;

    U u;
    u.vec = values;

    float flFirstVal = u.flArray[0];
    auto it = setResults.find( flFirstVal );
    if( it != setResults.end( ) )
    
        return it->second;
    

    __m128 prev = _mm_sqrt_ps( _mm_add_ps( values, _mm_sqrt_ps( values ) ) );
    __m128 curr = _mm_sqrt_ps( _mm_add_ps( values, prev ) );

    while( _mm_movemask_ps( _mm_cmplt_ps( _mm_sub_ps( curr, prev ), eps ) ) != 0xF )
    
        prev = curr;
        curr = _mm_sqrt_ps( _mm_add_ps( values, prev ) );
    

    setResults.insert( std::make_pair( flFirstVal, curr ) );
    return curr;

我正在使用以下代码循环调用该函数:

long long N;
std::cin >> N;

float flExpectation = 0.0f;
long long iMultipleOf4 = (N / 4LL) * 4LL;
for( long long i = iMultipleOf4; i > 0LL; i -= 4LL )

    __declspec(align(16)) float flArray[4] =  static_cast<float>(i - 3), static_cast<float>(i - 2), static_cast<float>(i - 1), static_cast<float>(i) ;
    __m128 arg = _mm_load_ps( flArray );
    __m128 vec = CalculateRadicals( arg );

    float flSum = Sum( vec );
    flExpectation += flSum;


for( long long i = iMultipleOf4; i < N; ++i )

    flExpectation += CalculateRadical( static_cast<float>(i), 0.0005f );


flExpectation /= N;

对于输入5,我得到以下输出:

With SSE version: 2.20873
With FPU verison: 1.69647

差异来自哪里,我在 SIMD 等效项中做错了什么?


编辑:我意识到 Sum 函数在这里是相关的:

float Sum( __m128 vec1 )

    float flTemp[4];
    _mm_storeu_ps( flTemp, vec1 );
    return flTemp[0] + flTemp[1] + flTemp[2] + flTemp[3];

【问题讨论】:

我想知道,您比较中的 0xF 是否被符号扩展为与 0x0F 不同的值? @BrianCain 将值更改为0x0F 不幸的是并没有改变结果!不过感谢您的建议! 为了稳健性,这个比较std::abs( tPrev - tCurr ) &gt; tEps 可能应该关心isnan。不过可能不是你的问题。 @BrianCain 是的,但我认为合同可以与参数传递者有关;只要0 &lt;= tValue &lt;= FL_MAX isnan 应该不是问题吧? 当然,除非isnan(tValue)。足够公平,调用者应该知道isnan(tValue)这个函数是否永远不会返回。 【参考方案1】:

SSE 内在函数有时可能相当乏味...

但不是在这里。你只是搞砸了你的循环:

for( long long i = iMultipleOf4; i > 0LL; i -= 4LL )

我怀疑它是否符合您的预期。如果iMultipleOf4 是 4,那么您的函数将使用 4,3,2,1 而不是 0 进行计算。然后您的第二个循环使用 4 重做计算。

这两个函数对我来说给出了相同的结果,而循环在更正后给出了相同的flExpectation。尽管仍然存在细微差别,可能是因为 FPU 在计算方式上略有不同。

【讨论】:

非常感谢;这似乎解决了我的问题!然而;我很好奇为什么值会有如此大的差异,我没想到前5个有效数字会有差异;你知道我能做些什么来帮助纠正这个问题吗? (我需要精确到 10^-3 的解)? @Shaktal 我检查了(在我的机器上)到底发生了什么,似乎std::sqrt 正在使用非向量双精度 SSE 进行计算。您的 SSE 代码使用矢量单精度,因此您可能会得到不太精确的结果。匹配计算的唯一方法可能是使用双精度。 @Shaktal 与您的问题无关,自 2011 年以来的 CPU 具有 AVX 指令,可启用 256 位宽的向量,而不是您使用的 128 位。如果你想使用它,你应该在运行时检查 AVX 支持。此外,2015 年,新的 CPU 将支持 AVX-512。 那么您是说std::sqrt 会将我传递给它的float 值转换为double,然后使用内部双精度sqrt FPU 函数,然后再转换回来? @Shaktal 是的。当我编译你的代码时,程序集将浮点数加载到 SSE 寄存器中,转换为双精度,SSE 双精度平方,然后转换为单精度。这可能因编译器、版本和架构而异,但这就是我得到的。 (我在 Windows 8.1 amd64 上使用过 VS2013)

以上是关于修改函数以使用 SSE 内在函数的主要内容,如果未能解决你的问题,请参考以下文章

SSE 内在函数优化

数组乘法与 sse 内在函数乘法的时序?

如何将参数传递给英特尔 SSE 内在函数中的 const 值?

用 sse 执行内在函数

SSE 内在函数向右移位

内在函数和寄存器(SSE)