使用 Vector<T> 和 SIMD 技术向量化的 C# 代码运行速度比传统循环慢。

4
我看到了一些文章描述了如何使用JIT内置函数实现Vector<T>的SIMD功能,因此在使用它时,编译器会正确输出AVS/SSE等指令,比传统的线性循环(例如这里)更快。
我决定尝试重写一个方法,以查看是否能够获得一些加速,但目前为止,向量化代码运行速度比原始代码慢3倍,我不确定原因。以下是两个版本的一个方法,检查两个Span<float>实例是否具有相同位置上共享相同阈值的所有成对项。
// Classic implementation
public static unsafe bool MatchElementwiseThreshold(this Span<float> x1, Span<float> x2, float threshold)
{
    fixed (float* px1 = &x1.DangerousGetPinnableReference(), px2 = &x2.DangerousGetPinnableReference())
        for (int i = 0; i < x1.Length; i++)
            if (px1[i] > threshold != px2[i] > threshold)
                return false;
    return true;
}

// Vectorized
public static unsafe bool MatchElementwiseThresholdSIMD(this Span<float> x1, Span<float> x2, float threshold)
{
    // Setup the test vector
    int l = Vector<float>.Count;
    float* arr = stackalloc float[l];
    for (int i = 0; i < l; i++)
        arr[i] = threshold;
    Vector<float> cmp = Unsafe.Read<Vector<float>>(arr);
    fixed (float* px1 = &x1.DangerousGetPinnableReference(), px2 = &x2.DangerousGetPinnableReference())
    {
        // Iterate in chunks
        int
            div = x1.Length / l,
            mod = x1.Length % l,
            i = 0,
            offset = 0;
        for (; i < div; i += 1, offset += l)
        {
            Vector<float>
                v1 = Unsafe.Read<Vector<float>>(px1 + offset),
                v1cmp = Vector.GreaterThan<float>(v1, cmp),
                v2 = Unsafe.Read<Vector<float>>(px2 + offset),
                v2cmp = Vector.GreaterThan<float>(v2, cmp);
            float*
                pcmp1 = (float*)Unsafe.AsPointer(ref v1cmp),
                pcmp2 = (float*)Unsafe.AsPointer(ref v2cmp);
            for (int j = 0; j < l; j++)
                if (pcmp1[j] == 0 != (pcmp2[j] == 0))
                    return false;
        }

        // Test the remaining items, if any
        if (mod == 0) return true;
        for (i = x1.Length - mod; i < x1.Length; i++)
            if (px1[i] > threshold != px2[i] > threshold)
                return false;
    }
    return true;
}

正如我所说,我使用BenchmarkDotNet测试了两个版本,使用Vector<T>的版本比另一个版本慢大约3倍。我尝试使用不同长度的区间运行测试(从大约100到2000以上),但向量化方法仍然比另一个版本慢得多。
我是否遗漏了一些显而易见的东西?
谢谢!
编辑:我使用不安全代码并尽可能地优化此代码,而不是将其并行化的原因是该方法已经从Parallel.For迭代中调用。
此外,能够在多个线程上并行处理代码通常不是让单个并行任务未经优化的好理由。

2
仅从我的个人经验来看,我会使用Parallel.For进行多线程处理,而不是使用不安全的代码来加速我的代码。 - Gordon
1
@Gordon 我已经在使用 Parallel.For,这个方法实际上将在每个并行迭代中被调用。 - Sergio0694
2
如果你真的对性能感兴趣,你可能想考虑将你的代码移植到C++,在那里你可以使用.NET不支持(至少从程序员的角度来看)的功能,如SSE2及以上。通过c++/CLI或直接使用p-invoke进行桥接。在C#中过度使用unsafe和指针就像与语言作斗争。 - user585968
1
@MickyD我已经在我的库中使用了GPU加速,但仍有一个只能在没有CUDA GPU时使用的纯CPU部分,我希望尽可能地优化它。如果我的先前评论发表得有些不当,我很抱歉,我并不是要显得无礼或者恼怒,我只是想解释一下我为什么对这种方式进行代码优化感兴趣——事实上,你的观察完全正确。 - Sergio0694
1
一切都很好,先生。您的项目非常令人兴奋。祝您一切顺利。 :) - user585968
显示剩余8条评论
3个回答

2

我曾经遇到过同样的问题。解决办法是在项目属性中取消勾选优先考虑32位选项。

SIMD仅适用于64位进程。因此,确保应用程序直接针对x64进行目标设置,或者编译为任何CPU并且未标记为优先考虑32位。 [来源]


1

**编辑**阅读了Marc Gravell的博客文章之后,我发现这可以简单地实现...

public static bool MatchElementwiseThresholdSIMD(ReadOnlySpan<float> x1, ReadOnlySpan<float> x2, float threshold)
{
    if (x1.Length != x2.Length) throw new ArgumentException("x1.Length != x2.Length");

    if (Vector.IsHardwareAccelerated)
    {
        var vx1 = x1.NonPortableCast<float, Vector<float>>();
        var vx2 = x2.NonPortableCast<float, Vector<float>>();

        var vthreshold = new Vector<float>(threshold);
        for (int i = 0; i < vx1.Length; ++i)
        {
            var v1cmp = Vector.GreaterThan(vx1[i], vthreshold);
            var v2cmp = Vector.GreaterThan(vx2[i], vthreshold);
            if (Vector.Xor(v1cmp, v2cmp) != Vector<int>.Zero)
                return false;
        }

        x1 = x1.Slice(Vector<float>.Count * vx1.Length);
        x2 = x2.Slice(Vector<float>.Count * vx2.Length);
    }

    for (var i = 0; i < x1.Length; i++)
        if (x1[i] > threshold != x2[i] > threshold)
            return false;

    return true;
}

现在,这种方法并不像直接使用数组那样快(如果您有的话),但仍然比非SIMD版本快得多...

(另一个编辑...)

...只是为了好玩,我想看看当完全通用时,这些东西处理起来如何,答案是非常好的...因此,您可以编写以下代码,并且与具体指定一样高效(好吧,除了在非硬件加速情况下,它会慢大约两倍,但并非完全糟糕...)

    public static bool MatchElementwiseThreshold<T>(ReadOnlySpan<T> x1, ReadOnlySpan<T> x2, T threshold)
        where T : struct
    {
        if (x1.Length != x2.Length)
            throw new ArgumentException("x1.Length != x2.Length");

        if (Vector.IsHardwareAccelerated)
        {
            var vx1 = x1.NonPortableCast<T, Vector<T>>();
            var vx2 = x2.NonPortableCast<T, Vector<T>>();

            var vthreshold = new Vector<T>(threshold);
            for (int i = 0; i < vx1.Length; ++i)
            {
                var v1cmp = Vector.GreaterThan(vx1[i], vthreshold);
                var v2cmp = Vector.GreaterThan(vx2[i], vthreshold);
                if (Vector.AsVectorInt32(Vector.Xor(v1cmp, v2cmp)) != Vector<int>.Zero)
                    return false;
            }

            // slice them to handling remaining elementss
            x1 = x1.Slice(Vector<T>.Count * vx1.Length);
            x2 = x2.Slice(Vector<T>.Count * vx1.Length);
        }

        var comparer = System.Collections.Generic.Comparer<T>.Default;
        for (int i = 0; i < x1.Length; i++)
            if ((comparer.Compare(x1[i], threshold) > 0) != (comparer.Compare(x2[i], threshold) > 0))
                return false;

        return true;
    }

“不太快,直接使用数组会更快一些”,你是在说向量化仍然不能提供速度提升吗? - Qwertie

0

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