单声道SIMD性能恶化?

时间:2012-01-02 20:29:52

标签: c# mono monodevelop vectorization simd

基准代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Mono.Simd;
using MathNet.Numerics.LinearAlgebra.Single;

namespace XXX {
public static class TimeSpanExtensions {
    public static double TotalNanoseconds(this TimeSpan timeSpan) {
        return timeSpan.TotalMilliseconds * 1000000.0;
    }
}

public sealed class SimdBenchmark : Benchmark {
    Vector4f a = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f);
    Vector4f b = new Vector4f(1.0f, 2.0f, 3.0f, 4.0f);
    Vector4f c;

    public override void Do() {
        c = a + b;
    }
}

public sealed class MathNetBenchmark : Benchmark {
    DenseVector a = new DenseVector(new float[]{1.0f,2.0f,3.0f,4.0f});
    DenseVector b = new DenseVector(new float[]{1.0f,2.0f,3.0f,4.0f});
    DenseVector c;

    public override void Do() {
        c = a + b;
    }
}

public sealed class DefaultBenchmark : Benchmark {
    Vector4 a = new Vector4(1.0f, 2.0f, 3.0f, 4.0f);
    Vector4 b = new Vector4(1.0f, 2.0f, 3.0f, 4.0f);
    Vector4 c;

    public override void Do() {
        c = a + b;
    }
}

public sealed class SimpleBenchmark : Benchmark {
    float a = 1.0f;
    float b = 2.0f;
    float c;

    public override void Do() {
        c = a + b;
    }
}

public sealed class DelegateBenchmark : Benchmark {
    private readonly Action _action;

    public DelegateBenchmark(Action action) {
        _action = action;
    }

    public override void Do() {
        _action();
    }
}

public abstract class Benchmark : IEnumerable<TimeSpan> {
    public IEnumerator<TimeSpan> GetEnumerator() {
        Do(); // Warm-up!

        GC.Collect(); // Collect garbage.
        GC.WaitForPendingFinalizers(); // Wait until finalizers finish.

        var stopwatch = new Stopwatch();

        while (true) {
            stopwatch.Reset();
            stopwatch.Start();
            Do();
            stopwatch.Stop();

            yield return stopwatch.Elapsed;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

    public abstract void Do();
}

public struct Vector4 {
    float x;
    float y;
    float z;
    float w;

    public Vector4(float x, float y, float z, float w) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
    }

    public static Vector4 operator +(Vector4 v1, Vector4 v2) {
        return new Vector4(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, v1.w + v2.w);
    }
}

class MainClass {
    public static void Main(string[] args) {
        var avgNS1 = new SimdBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS2 = new SimpleBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS3 = new DefaultBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS4 = new MathNetBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());


        Console.WriteLine(avgNS1 + " ns");
        Console.WriteLine(avgNS2 + " ns");
        Console.WriteLine(avgNS3 + " ns");
        Console.WriteLine(avgNS4 + " ns");
    }
}
}

环境设置:

Windows 7 / Mono 2.10.8 / MonoDevelop 2.8.5

MonoDevelop设置:

  • 工具&gt;选项&gt; .NET运行时&gt;单声道2.10.8(默认)
  • 项目&gt;选项&gt;构建&gt;一般&gt;目标框架&gt;单声道/ .NET 4.0
  • 项目&gt;选项&gt;构建&gt;编译器&gt;一般选项&gt;启用优化
  • 项目&gt;选项&gt;构建&gt;编译器&gt;一般选项&gt;平台目标&gt; 86
  • 项目&gt;选项&gt;运行&gt;一般&gt;参数&gt; -O = SIMD

结果:

  • 94.4 ns
  • 29.7 ns
  • 49.9 ns
  • 231595.2 ns

3 个答案:

答案 0 :(得分:6)

我会首先怀疑您的基准测试基础架构。

有几点可能是:

  • 您正在使用“秒表”计时单次操作 - 它没有分辨率
  • 您的时间安排包括虚拟功能调用
  • 您的样本量(1000)太小

答案 1 :(得分:6)

这些是我的结果:

1608.8 ns
1554.9 ns
1582.5 ns

(没有MathNET,虽然这里不重要)。 Os是Ubuntu 10.10(32位),Mono 2.10.7。此时您可能会考虑制作针对Windows Mono版本的错误报告。但是:

我认为由于基准测试的机制开销,这不是对SIMD操作进行基准测试的正确方法。

例如,根据您的Vector4类来查看此原始测试。

        const int count = 100000;
        var simdVector = new Vector4f(1, 2, 3, 4);
        var simdResult = simdVector;
        var sw = Stopwatch.StartNew();
        for(var i = 0; i < count; i++)
        {
            simdResult += simdVector;
        }
        sw.Stop();
        Console.WriteLine("SIMD  result: {0} {1}", sw.Elapsed, simdResult);
        sw = Stopwatch.StartNew();
        var usualVector = new Vector4(1, 2, 3, 4);
        var usualResult = usualVector;
        for(var i = 0; i < count; i++)
        {
            usualResult += usualVector;
        }
        sw.Stop();
        Console.WriteLine("Usual result: {0} {1}", sw.Elapsed, usualResult);

在我的机器上,结果是:

SIMD  result: 00:00:00.0005802 <100001, 200002, 300003, 400004>
Usual result: 00:00:00.0029598 <100001, 200002, 300003, 400004>

所以肯定不同于你的测试。因此,您可能认为SIMD操作的速度更快 - 但基准测试并不容易。在这种配置中上环更快的原因有很多。这些原因可以在另一个场合讨论。

然而,确定SIMD 比连续几次添加更快。您应该检查的是它们是否真的被释放。

在Linux上,可以使用mono -v -v检查生成的程序集(在目标处理程序的程序集的意义上,而不是单声道程序集;))。然而,我不确定它是否适用于通常的Windows系统,因为它可能使用来自GCC的disas(你可能有更多的运气使用cygwin)。通过阅读这样的程序集,您可以检查是否真正发出了SIMD操作。

例如,通过检查为上面粘贴的程序生成的程序集,可以发现它在SIMD循环中使用addps指令,这是我们在这里寻找的。

哦,为了完整性,这里输出SIMD禁用:

$ mono --optimize=-simd SimdTest.exe 
SIMD result: 00:00:00.0027111 <100001, 200002, 300003, 400004>
Usual result: 00:00:00.0026127 <100001, 200002, 300003, 400004>

与生成的程序集不太重要,不包含SIMD操作。

希望这有用。

答案 2 :(得分:1)

好吧,我已经设法修改我的基准代码,使其更强大,完全无偏见。换句话说:

首先,正如我们与尼古拉斯讨论的那样 - 测量单一操作可能会产生扭曲的结果。此外,由于秒表的频率为1000万 - 这意味着每100秒发生一次滴答。因此,考虑到这一事实,以前的结果看起来很奇怪。因此,为了缓解这个问题,我决定测试1000次操作而不是1次。

其次,我并不完全确定,但我猜之前的基准实现受到密集缓存,因为在每次迭代时,总和都是在相同的向量之间计算的(它们的组件从未改变过)。我看到的唯一直接解决方案是在每次测试之前简单地使用随机组件重建矢量。

各自的基准实施是:

public static class TimeSpanExtensions {
    public static double TotalNanoseconds(this TimeSpan timeSpan) {
        return timeSpan.TotalMilliseconds * 1000000.0;
    }
}

public static class RandomExtensions {
    public static float NextFloat(this Random random) {
        return (float)random.NextDouble();
    }

    public static float NextFloat(this Random random, float min, float max) {
        return random.NextFloat() * (max - min) + min;
    }
}

public sealed class SimdBenchmark : Benchmark {
    Vector4f[] a = new Vector4f[1000];
    Vector4f[] b = new Vector4f[1000];
    Vector4f[] c = new Vector4f[1000];

    public override void Begin() {
        Random r = new Random();

        for (int i = 0; i < 1000; ++i) {
            a[i] = new Vector4f(r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat());
            b[i] = new Vector4f(r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat());
        }
    }

    public override void Do() {
        for (int i = 0; i < 1000; ++i)
            c[i] = a[i] + b[i];
    }

    public override void End() {

    }
}

public sealed class MathNetBenchmark : Benchmark {
    DenseVector[] a = new DenseVector[1000];
    DenseVector[] b = new DenseVector[1000];
    DenseVector[] c = new DenseVector[1000];

    public override void Begin() {
        Random r = new Random();

        for (int i = 0; i < 1000; ++i) {
            a[i] = new DenseVector(new float[]{r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat()});
            b[i] = new DenseVector(new float[]{r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat()});
        }
    }

    public override void Do() {
        for (int i = 0; i < 1000; ++i)
            c[i] = a[i] + b[i];
    }

    public override void End() {

    }
}

public sealed class DefaultBenchmark : Benchmark {
    Vector4[] a = new Vector4[1000];
    Vector4[] b = new Vector4[1000];
    Vector4[] c = new Vector4[1000];

    public override void Begin() {
        Random r = new Random();

        for (int i = 0; i < 1000; ++i) {
            a[i] = new Vector4(r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat());
            b[i] = new Vector4(r.NextFloat(), r.NextFloat(), r.NextFloat(), r.NextFloat());
        }
    }

    public override void Do() {
        for (int i = 0; i < 1000; ++i)
            c[i] = a[i] + b[i];
    }

    public override void End() {

    }
}

public sealed class SimpleBenchmark : Benchmark {
    float[] a = new float[1000];
    float[] b = new float[1000];
    float[] c = new float[1000];

    public override void Begin() {
        Random r = new Random();

        for (int i = 0; i < 1000; ++i) {
            a[i] = r.NextFloat();
            b[i] = r.NextFloat();
        }
    }

    public override void Do() {
        for (int i = 0; i < 1000; ++i)
            c[i] = a[i] + b[i];
    }

    public override void End() {

    }
}

public sealed class DelegateBenchmark : Benchmark {
    private readonly Action _action;

    public DelegateBenchmark(Action action) {
        _action = action;
    }

    public override void Begin() {

    }

    public override void Do() {
        _action();
    }

    public override void End() {

    }
}

public abstract class Benchmark : IEnumerable<TimeSpan> {
    public IEnumerator<TimeSpan> GetEnumerator() {
        Begin();
        Do(); // Warm-up!
        End();

        var stopwatch = new Stopwatch();

        while (true) {
            Begin();

            GC.Collect(); // Collect garbage.
            GC.WaitForPendingFinalizers(); // Wait until finalizers finish.

            stopwatch.Reset();
            stopwatch.Start();

            Do();

            stopwatch.Stop();

            End();

            yield return stopwatch.Elapsed;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }

    public abstract void Begin();

    public abstract void Do();

    public abstract void End();
}

public struct Vector4 {
    float x;
    float y;
    float z;
    float w;

    public Vector4(float x, float y, float z, float w) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
    }

    public static Vector4 operator +(Vector4 v1, Vector4 v2) {
        return new Vector4(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, v1.w + v2.w);
    }
}

class MainClass {
    public static void Main(string[] args) {
        var avgNS1 = new SimdBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS2 = new SimpleBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS3 = new DefaultBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());
        var avgNS4 = new MathNetBenchmark().Take(1000).Average(timeSpan => timeSpan.TotalNanoseconds());

        Console.WriteLine(avgNS1 + " ns");
        Console.WriteLine(avgNS2 + " ns");
        Console.WriteLine(avgNS3 + " ns");
        Console.WriteLine(avgNS4 + " ns");
    }
}

<强>结果:

  • 3203.9 ns
  • 2677.4 ns
  • 20138.4 ns
  • 597581060.7 ns

我认为它证实了SIMD正在播出,因为SimdBenchmark接近SimpleBenchmark(正如SIMD技术所预期的那样)并且比DefaultBenchmark好得多(再次如SIMD技术所暗示的那样)。

此外,结果似乎与konrad.kruczynski一致,因为SimdBenchmark(3203.9)和DefaultBenchmark(20138.4)之间的比率约为6,而simdVector(5802)和ordinaryVector(29598)之间的比率也约为6。

无论如何仍然存在2个问题:

  1. 为什么玩“-O = simd”/“-O = -simd”无效。它被弃用了吗? SIMD会自动启用吗?
  2. 具有100 ns刻度的秒表怎么能给出以前的结果(94.4,29.7,49.9),这显然低于100 ns?