【问题标题】:Mono SIMD worsening performance?单 SIMD 性能恶化?
【发布时间】:2012-02-01 01:34:41
【问题描述】:

基准代码:

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 设置:

  • 工具 > 选项 > .NET 运行时 > Mono 2.10.8(默认)
  • 项目 > 选项 > 构建 > 常规 > 目标框架 > Mono / .NET 4.0
  • 项目 > 选项 > 构建 > 编译器 > 常规选项 > 启用优化
  • 项目 > 选项 > 构建 > 编译器 > 常规选项 > 平台目标 > x86
  • 项目 > 选项 > 运行 > 常规 > 参数 > -O=simd

结果:

  • 94.4 ns
  • 29.7 纳秒
  • 49.9 ns
  • 231595.2 ns

【问题讨论】:

  • 至于您的编辑 1:不一定。只是为了你,我已经在 Windows 7 机器上的 Mono 和 binutils 上安装了你的版本,以检查使用 -O=simd 和 -O=-simd 生成的本机代码。事实证明,SIMD 支持在这两种情况下都适用;) 您可以在 Mono 的 bugzilla 上发布一个错误,即不能在 Windows 上使用 -O 或 --optimize 标志禁用 SIMD支持。您还可以在您的回答下看到我的最后一条评论。

标签: c# mono monodevelop vectorization simd


【解决方案1】:

我会首先怀疑您的基准基础设施。

有几点可能是:

  • 您正在使用“秒表”来计时单个操作 - 它没有分辨率
  • 您的计时包括虚函数调用
  • 您的样本量 (1000) 太小

【讨论】:

  • 虚拟调用开销非常小 - 已经检查过了。如果秒表没有分辨率,那么我们将在任何地方得到相同的结果(当然除了可怕的 MathNet )。 1000 足以确定 SimdBenchmark 是否至少大致接近 SimpleBecnhmark(正如技术所暗示的那样)并且明显优于 DefaultBenchmark(也暗示)。
  • 嘿!只是想在这里提供帮助:) 您的结果在测试运行中是否一致? Stopwatch.Frequency 返回什么?
  • 是的,它们大部分时间都是一致的(比如 9/10)。频率为 1000 万次 - 这意味着每 100 ns 发生一次滴答声(考虑到结果,这实际上很奇怪)... xD
【解决方案2】:

这些是我的结果:

1608.8 ns
1554.9 ns
1582.5 ns

(没有 MathNET,尽管在这里并不重要)。操作系统是 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 指令,这正是我们在这里寻找的。​​p>

哦,为了完整起见,这里是禁用 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 操作。

希望这对您有所帮助。

【讨论】:

    【解决方案3】:

    嗯,我已经设法修改了我的基准代码,使其更加健壮和完全公正。换句话说:

    首先,正如我们与 Nicholas 讨论的那样 - 测量单个操作可能会产生扭曲的结果。此外,由于秒表的频率为 1000 万次 - 这意味着每 100 ns 发生一次滴答声。所以考虑到这个事实,以前的结果看起来很奇怪。因此,为了缓解这个问题,我决定一次测试 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 纳秒
    • 2677.4 纳秒
    • 20138.4 ns
    • 597581060.7 ns

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

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

    仍然有 2 个问题:

    1. 为什么玩“-O=simd”/“-O=-simd”没有效果。是否已弃用? SIMD 会自动启用吗?
    2. 具有 100 ns 滴答声的秒表如何给出明显低于 100 ns 的先前结果(94.4、29.7、49.9)?

    【讨论】:

    • 第一个问题:请阅读我从“哦,为了完整性”的回答,它适用于我的机器。您可以使用 --optimize=-simd 检查是否仍然发出 SIMD,如果是,请发布错误。
    • 他们是否从“O”切换到“优化”?
    • 不,-O 在我的操作系统上也能正常工作。我不确定我是否理解正确,但默认情况下启用了 simd,但是您应该能够使用 -O=-simd 或 --optimize=-simd 禁用它们。至于你的第二个问题:这将是非常奇怪的不管秒表频率,因为你正在使用 Elapsed 属性。 Elapsed 属于 TimeSpan 类型,最小时间跨度是一个 TimeSpan tick = 100 ns。您观察到的值的原因是平均。打印所有值而不是平均值,您应该不会注意到任何异常。
    • 顺便说一句,您应该从 Ticks 中提取 TotalNanoseconds 而不是从 TotalMilliseconds 中提取。
    • 秒表使用它的频率和刻度将正确的值映射到时间跨度。谈到平均:好吧,如果我的 N 值 >= 100 ns(我们知道我们无法更精确地测量),那么平均它们的总和永远不会产生
    猜你喜欢
    • 2016-07-28
    • 2017-03-18
    • 1970-01-01
    • 2017-12-04
    • 2021-06-09
    • 1970-01-01
    • 2018-12-15
    • 2018-03-30
    • 2014-12-04
    相关资源
    最近更新 更多