SIMD还是非SIMD——跨平台

15

我需要一些想法来编写一个C++跨平台实现的可并行化问题,以便在可能的情况下利用SIMD(SSE、SPU等),并且还希望能够在运行时切换使用SIMD和不使用SIMD。

你会建议我如何解决这个问题? (当然,我不想为所有可能的选项多次实现该问题)

我可以看到这可能不是很容易的任务,但我相信我还缺少了一些东西。到目前为止,我的想法是这样的...... 一个类cStream将成为单个字段的数组。使用多个cStream,我可以实现SoA(结构体数组)。然后使用几个Functors,我可以模拟我需要在整个cStream上执行的Lambda函数。

// just for example I'm not expecting this code to compile
cStream a; // something like float[1024]
cStream b;
cStream c;

void Foo()
{
    for_each(
        AssignSIMD(c, MulSIMD(AddSIMD(a, b), a)));
}

for_each将负责递增流的当前指针,并使用SIMD和非SIMD内联执行函数对象的主体。

类似以下方式:

// just for example I'm not expecting this code to compile
for_each(functor<T> f)
{
#ifdef USE_SIMD
    if (simdEnabled)
        real_for_each(f<true>()); // true means use SIMD
    else
#endif
        real_for_each(f<false>());
}

注意,如果启用了SIMD,则只需检查一次,并且循环围绕主函子。


2
请查看libsimdpp库 - 它几乎可以满足您的需求。 您只需要编写一次算法:相同的源代码可以使用不同的编译器选项进行多次编译(命名空间负责ODR),链接到同一个可执行文件中,库将自动为目标处理器选择最佳实现。(免责声明:我是作者) - user12
6个回答

5

如果有人感兴趣,这是我用来测试新想法的肮脏代码,当我阅读Paul发布的库时,我想到了它。

谢谢Paul!

// This is just a conceptual test
// I haven't profile the code and I haven't verified if the result is correct
#include <xmmintrin.h>


// This class is doing all the math
template <bool SIMD>
class cStreamF32
{
private:
    void*       m_data;
    void*       m_dataEnd;
    __m128*     m_current128;
    float*      m_current32;

public:
    cStreamF32(int size)
    {
        if (SIMD)
            m_data = _mm_malloc(sizeof(float) * size, 16);
        else
            m_data = new float[size];
    }
    ~cStreamF32()
    {
        if (SIMD)
            _mm_free(m_data);
        else
            delete[] (float*)m_data;
    }

    inline void Begin()
    {
        if (SIMD)
            m_current128 = (__m128*)m_data;
        else
            m_current32 = (float*)m_data;
    }

    inline bool Next()
    {
        if (SIMD)
        {
            m_current128++;
            return m_current128 < m_dataEnd;
        }
        else
        {
            m_current32++;
            return m_current32 < m_dataEnd;
        }
    }

    inline void operator=(const __m128 x)
    {
        *m_current128 = x;
    }
    inline void operator=(const float x)
    {
        *m_current32 = x;
    }

    inline __m128 operator+(const cStreamF32<true>& x)
    {
        return _mm_add_ss(*m_current128, *x.m_current128);
    }
    inline float operator+(const cStreamF32<false>& x)
    {
        return *m_current32 + *x.m_current32;
    }

    inline __m128 operator+(const __m128 x)
    {
        return _mm_add_ss(*m_current128, x);
    }
    inline float operator+(const float x)
    {
        return *m_current32 + x;
    }

    inline __m128 operator*(const cStreamF32<true>& x)
    {
        return _mm_mul_ss(*m_current128, *x.m_current128);
    }
    inline float operator*(const cStreamF32<false>& x)
    {
        return *m_current32 * *x.m_current32;
    }

    inline __m128 operator*(const __m128 x)
    {
        return _mm_mul_ss(*m_current128, x);
    }
    inline float operator*(const float x)
    {
        return *m_current32 * x;
    }
};

// Executes both functors
template<class T1, class T2>
void Execute(T1& functor1, T2& functor2)
{
    functor1.Begin();
    do
    {
        functor1.Exec();
    }
    while (functor1.Next());

    functor2.Begin();
    do
    {
        functor2.Exec();
    }
    while (functor2.Next());
}

// This is the implementation of the problem
template <bool SIMD>
class cTestFunctor
{
private:
    cStreamF32<SIMD> a;
    cStreamF32<SIMD> b;
    cStreamF32<SIMD> c;

public:
    cTestFunctor() : a(1024), b(1024), c(1024) { }

    inline void Exec()
    {
        c = a + b * a;
    }

    inline void Begin()
    {
        a.Begin();
        b.Begin();
        c.Begin();
    }

    inline bool Next()
    {
        a.Next();
        b.Next();
        return c.Next();
    }
};


int main (int argc, char * const argv[]) 
{
    cTestFunctor<true> functor1;
    cTestFunctor<false> functor2;

    Execute(functor1, functor2);

    return 0;
}

3

1
MacSTL中有很多模板。我需要时间来弄清它是如何实现的。但是在阅读相关资料时,我想到了一个似乎可行的想法。如果其他人也感兴趣,我会发布一些不太规范的代码作为新答案... - Aleks
我很想看看你编写的任何代码。我已经有了这种问题的部分解决方案,但不幸的是它是专有的(知识产权属于我的雇主)。 - Paul R

2
您可能想看一下我尝试使用SIMD /非SIMD的结果:
  • vrep,一个带有SIMD特化的模板基类(请注意它如何区分仅浮点数的SSE和引入整数向量的SSE2)。

  • 更有用的v4fv4i等类(通过中间v4进行子类化)。

当然,它更适合于rgba/xyz类型的4元素向量计算,而不是SoA,因此当8路AVX出现时,它将完全失效,但一般原则可能会有用。

这是一个有趣的方法,我在我的情况下确实需要SoA。但也许我会尝试做模板特化。 - Aleks

2
最引人注目的SIMD缩放方法我见过的是RTFact光线追踪框架:幻灯片, 论文。非常值得一看。研究人员与英特尔密切相关(萨尔布吕肯现在承办英特尔视觉计算研究所),因此可以确信他们考虑了将来将AVX和Larrabee扩展到前沿的问题。
英特尔的Ct“数据并行”模板库也很有前途。

1
请注意,给定的示例决定在编译时执行什么(因为您正在使用预处理器),在这种情况下,您可以使用更复杂的技术来决定实际要执行的内容;例如,标签分派:http://cplusplus.co.il/2010/01/03/tag-dispatching/ 根据那里展示的示例,您可以将快速实现与SIMD一起使用,而不使用慢速实现。

有三个问题。
  1. 在编译时做出决策
  2. 需要多个实现
  3. 决策基于输入数据,我希望在 SIMD 和非 SIMD 中使用相同的数据。
- Aleks

0

你有没有考虑使用像liboil这样的现有解决方案?它实现了许多常见的SIMD操作,并且可以在运行时决定是否使用SIMD /非SIMD代码(使用由初始化函数分配的函数指针)。


我仍然需要检查这个库如何使用函数指针在SIMD /非SIMD之间切换,但我不知道这些函数指针将如何内联。我注意到的另一个问题是所有算术函数都使用自己的循环实现。 for(i=0;i<(n&(~0x3));i+=4){ ... } for(;i<n;i++){ ... }对于像A + B + C这样的东西,它需要两次遍历所有元素。 - Aleks

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