Моно SIMD ухудшает производительность? - PullRequest
3 голосов
/ 03 января 2012

Код эталона:

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 Runtime> Mono 2.10.8 (по умолчанию)
  • Проект> Параметры> Сборка> Общие> Целевая среда> Mono / .NET 4,0
  • Проект> Параметры> Сборка> Компилятор> Общие параметры> Включить оптимизации
  • Проект> Параметры> Сборка> Компилятор> Общие параметры> Цель платформы> x86
  • Проект> Параметры> Выполнить> Общие> Параметры> -O = simd

Результаты:

  • 94,4 нс
  • 29,7 нс
  • 49,9 нс
  • 231595,2 нс

Ответы [ 3 ]

6 голосов
/ 03 января 2012

Вот мои результаты:

1608.8 ns
1554.9 ns
1582.5 ns

(без MathNET, хотя здесь это не важно). Os Ubuntu 10.10 (32bit), 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, так как он, вероятно, использует disas от GCC (возможно, вам больше повезет с использованием cygwin). Читая такую ​​сборку, вы можете проверить, действительно ли выполняются операции SIMD.

Например, изучая сборку, сгенерированную для вставленной выше программы, можно обнаружить, что она использует инструкцию addps в цикле SIMD, что мы и ищем здесь.

О, и для полноты здесь вывод с отключенной 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.

Надеюсь, это было полезно.

6 голосов
/ 03 января 2012

Сначала я подозреваю, что ваша инфраструктура эталонного теста.

Пара моментов может быть:

  • Вы используете «секундомер» для определения времени отдельных операций - он не имеетразрешение
  • Время включает виртуальный вызов функции
  • Размер вашей выборки (1000) слишком мал
1 голос
/ 03 января 2012

Что ж, мне удалось изменить мой тестовый код, чтобы сделать его более надежным и полностью беспристрастным . Другими словами:

Во-первых, как мы уже говорили с Николасом, измерение одной операции может дать искаженные результаты. Более того, так как частота секундомера составляет 10 миллионов - это означает, что тики появляются каждые 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 нс
  • 2677,4 нс
  • 20138,4 нс
  • 597581060,7 нс

Я думаю, это подтверждает, что SIMD находится в эфире, потому что SimdBenchmark приближается к SimpleBenchmark (как предусмотрено технологией SIMD) и намного лучше, чем DefaultBenchmark (опять же, как подразумевается технологией SIMD).

Более того, результаты, по-видимому, согласуются с konrad.kruczynski, потому что соотношение между SimdBenchmark (3203,9) и DefaultBenchmark (20138.4) составляет около 6, а соотношение между simdVector (5802) и обычным Vector (29598) также составляет около 6.

В любом случае, еще осталось 2 вопроса:

  1. Почему игра с "-O = simd" / "-O = -simd" не имеет никакого эффекта. Это устарело? SIMD включается автоматически?
  2. Как секундомер с тиками в 100 нс мог дать предыдущие результаты (94,4, 29,7, 49,9), которые явно ниже 100 нс?
...