пятница, 19 апреля 2024 г.

Apple Studio M2 Max performance numbers

 Just numbers

fractalgpu benchmark
Single-core tests
=================
Rendering time: 0,360s 173,81mis 'ab' N1000 256x256 @LyapRendererCpu
Rendering time: 1,012s 247,14mis 'ab' N1000 512x512 @LyapRendererCpu
Rendering time: 4,028s 248,25mis 'ab' N1000 1024x1024 @LyapRendererCpu
Multi-core tests
=================
Rendering time: 0,036s 1724,47mis 'ab' N1000 256x256 @LyapRendererMulticore`1[LyapRendererCpu]
Rendering time: 0,129s 1940,65mis 'ab' N1000 512x512 @LyapRendererMulticore`1[LyapRendererCpu]
Rendering time: 0,512s 1951,92mis 'ab' N1000 1024x1024 @LyapRendererMulticore`1[LyapRendererCpu]
Rendering time: 1,207s 2071,2mis 'ab' N2500 1024x1024 @LyapRendererMulticore`1[LyapRendererCpu]
Rendering time: 2,357s 2121,1mis 'ab' N5000 1024x1024 @LyapRendererMulticore`1[LyapRendererCpu]
Rendering time: 4,670s 2141,36mis 'ab' N10000 1024x1024 @LyapRendererMulticore`1[LyapRendererCpu]
GPU tests
=================
Rendering time: 0,240s 260,25mis 'ab' N1000 256x256 @LyapRendererOpenCl
Rendering time: 0,011s 23188,94mis 'ab' N1000 512x512 @LyapRendererOpenCl
Rendering time: 0,040s 24773,94mis 'ab' N1000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,049s 51389,57mis 'ab' N2500 1024x1024 @LyapRendererOpenCl
Rendering time: 0,056s 88720,12mis 'ab' N5000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,079s 126046,18mis 'ab' N10000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,142s 176048,9mis 'ab' N25000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,250s 200288,42mis 'ab' N50000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,549s 204839,68mis 'ab' N50000 1536x1536 @LyapRendererOpenCl
Rendering time: 0,979s 204209,37mis 'ab' N50000 2048x2048 @LyapRendererOpenCl
Rendering time: 3,937s 203194,57mis 'ab' N50000 4096x4096 @LyapRendererOpenCl

вторник, 2 января 2024 г.

Производительность Apple M1 Pro

Давно не было апдейта. Достал исходники, благо есть сервис, которых сохранил бэкап с Битбакета (тот просто потер все проекты на HG когда прекратил его поддержку).

Производительность CPU/GPU

На одном ядре ЦПУ получается 220mis, в Multicore(10) - 1550mis. GPU/OpenCL - 106000mis.

Single-core tests
=================
Rendering time: 0,387s 161,43mis 'ab' N1000 256x256 @LyapRendererCpu
Rendering time: 1,102s 226,92mis 'ab' N1000 512x512 @LyapRendererCpu
Rendering time: 4,390s 227,79mis 'ab' N1000 1024x1024 @LyapRendererCpu

Multi-core tests
=================
Rendering time: 0,048s 1302,71mis 'ab' N1000 256x256 @LyapRendererMulticore[CPU]
Rendering time: 0,171s 1458,93mis 'ab' N1000 512x512 @LyapRendererMulticore[CPU]
Rendering time: 0,673s 1485,36mis 'ab' N1000 1024x1024 @LyapRendererMulticore[CPU]
Rendering time: 1,639s 1525,1mis 'ab' N2500 1024x1024 @LyapRendererMulticore[CPU]
Rendering time: 3,221s 1552,16mis 'ab' N5000 1024x1024 @LyapRendererMulticore[CPU]

GPU tests
=================
Rendering time: 0,239s 261,54mis 'ab' N1000 256x256 @LyapRendererOpenCl
Rendering time: 0,020s 12722,65mis 'ab' N1000 512x512 @LyapRendererOpenCl
Rendering time: 0,071s 14086,69mis 'ab' N1000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,087s 28611,32mis 'ab' N2500 1024x1024 @LyapRendererOpenCl
Rendering time: 0,101s 49483,88mis 'ab' N5000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,130s 76657,14mis 'ab' N10000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,259s 96520,25mis 'ab' N25000 1024x1024 @LyapRendererOpenCl
Rendering time: 0,489s 102167,8mis 'ab' N50000 1024x1024 @LyapRendererOpenCl
Rendering time: 1,063s 105882,25mis 'ab' N50000 1536x1536 @LyapRendererOpenCl
Rendering time: 1,902s 105130,36mis 'ab' N50000 2048x2048 @LyapRendererOpenCl
Rendering time: 7,616s 105041,71mis 'ab' N50000 4096x4096 @LyapRendererOpenCl

Удивительно быстро. Исходники теперь здесь OlegZee/fractalgpu (github.com).

среда, 2 ноября 2016 г.

Сравнение Core2 Duo 6400 и Core i5-6600

В начале года заапгрейдил домашний компьютер до Core i5 и вот решил сравнить цифры на FractalGPU. На новом получаю 160 mis.
Арифметика такая получается: 2x3.0GHz vs 4x4.5GHz = 40 mis vs 160 mis. То есть новые архитектуры CPU быстрее в (160/18) / 40/6 = 4/3 раза на один гигагерц-ядро. Очень неплохо.

суббота, 16 июля 2011 г.

Вычисления на GPU в .NET приложениях

В 1991 году мне в руки попался журнал Scientific American [1] с красивыми фрактальными картинками. В 90-х фракталы были в моде, даже растровый редактор был назван Fractal Design Painter, а в журналах обсуждались фрактальные алгоритмы сжатия изображений с умопомрачительной эффективностью (не менее 1000:1). В итоге оказалось что алгоритмы показывают обещанные результаты только для изображений типа листа папоротника.
Что касается картинок. Я написал программу и запускал ее в учебном классе на PC 8088, а преподаватель дал погонять на 80486 (с встроенным сопроцессором) в лаборатории в институте. Сейчас трудно вспомнить результаты, приблизительно 320x200 за 5 минут, это на 8088 но с сопроцессором.
Пару лет назад, с появлением DirectX 10, GPU, CUDA, захотелось посмотреть что же дал прогресс за последние 20 лет, а тут еще попалась пара интересных статей про MS Accelerator. Так как во фракталах Ляпунова значение каждой точки вычисляется независимо, то есть вычисления идеально распараллеливаются - задача как раз для GPU.

Первые цифры

Метрику для обозначения производительности возьмем такую - "миллион точка-итераций в секунду" (mega iterations per second, mis). Это отражает особенность алгоритма - в большинстве случаев сходимость достигается после 500 итераций, на значительная часть областей требует 2-4 тыс. итераций.
Исходный код выглядит так:
protected static double CalculateExponent(double[] pattern, double initial, int warmup, int iterations)
{
 var x = initial;
 var patternSize = pattern.Length;

 for (var i = 0; i < warmup; i++)
 {
  var r = pattern[i % patternSize];
  x *= r * (1 - x);
 }

 double total = 0;
 for (var i = warmup; i < iterations; i++)
 {
  var r = pattern[i % patternSize];
  var d = Math.Log(Math.Abs(r - 2 * r * x));

  total += d;
  if (Double.IsNaN(d) || Double.IsNegativeInfinity(d) || Double.IsPositiveInfinity(d))
  {
   return d;
  }

  x *= r * (1 - x);
 }

 return total / Math.Log(2) / (iterations - warmup);
}


Многопоточная реализация для CPU без использования SSE показала 42 mis на двухядерном Core2 6400 @3.0GHz. Немного, с учетом примерно 12 операций выборки/вычисления на цикл, или ~500 млн на два ядра. Около 10 тактов на операцию, что слишком плохо. Вероятно Log() и арифметика в таком режиме плохо конвейеризируются. Это отдельная задача для исследований.

MS Accelerator

Следом была написана реализация под Accelerator v.2, очень простая библиотека в идеологии SIMD, где формула выражение строится над большой матрицей. Библиотека содержит аналоги практически всех функций System.Math и перегружает операторы. Адресация соседних ячеек возможна с помощью функций сдвига матрицы. Библиотека действительно очень проста, изучается за один вечер и позволяет писать наглядный код типа:
var dimensions = new[] { 1000, 1000 };
var x = new FPA((float)settings.InitialValue, dimensions);

// creating A,B arrays on the fly a few times faster!
var fr = new Func<int, FPA>(i => Math.Replicate(new FPA(
 settings.Pattern[i % settings.Pattern.Length] == 'a' ? aArray : bArray), w, h));

// warmup cycle, no limit calculation
for (var i = 0; i < settings.Warmup; i++)
{
 var r = fr(i);
 x *= r * (1.0f - x);
}

var total = new FPA(0, dimensions);
for (var i = settings.Warmup; i < settings.Iterations; i++)
{
 var r = fr(i);

 total += Math.Log2(Math.Abs(r - 2 * r * x));
 x *= r - r * x;
}
total *= 1f / (settings.Iterations - settings.Warmup);

return total;

Результат в среднем 250 mis, но на некоторых разрешениях получается до 400 mis, то есть быстрее почти в 10 раз.
На этом можно было бы остановиться, но от видеокарты со 192 процессорами, работающими на частоте 1 ГГЦ ожидаешь хотя бы 100-кратного преимущества. Также были замечены разные странности, частично отраженные в вышеприведенном коде, и в частности, чрезмерное использование памяти видеокарты.

В результате, проштудировав соответствующую статью на Хабре, я решил попробовать библиотеку Brahma (она же за деньги под названием Tidepowered).

Brahma

Собственно код я написал и он доступен в исходниках. Но мне не хотелось переезжать на тормозную VS 2010, а на компиляторе C# 3.5 из-под VS2008 выражения (expression) получались, как выяснилось, неразборчивыми для Brahma. После пары часов упражнений я решил что мне эта прослойка не нужна и я быстрее напишу нативный код для OpenCL.
Смысл использования Brahma/Tidepowerd мне непонятен - документации мало, денег стоит много, синтаксис странный, кроссплатформенность неважная (например, вышеупомянутые проблемы на .NET 3.5), диагностика в случае ошибок никакая. Хотя код, в общем-то, нагляден:
(ds, a, b, m, t) =>
 from pt in ds
 let i = pt.GlobalID0
 let j = pt.GlobalID1
 let x = initialX
 let r = default(float32)
 let bv = b[i]
 let av = a[j]
 let warmup = engine.Loop(0, warmupCount, idxs =>
  idxs.Select(idx =>
   new Set[]
    {
     r <= (m[idx%maskLen] == 0 ? av : bv),
     x <= r*x - r*x*x
    })
  )
 let total = default(float32)
 let calculate = engine.Loop(warmupCount, iterationsCount, idxs =>
  idxs.Select(idx =>
   new Set[]
    {
     r <= (m[idx%maskLen] == 0 ? av : bv),
     total <= total + Math.Log(Math.Fabs(r - 2*r*x)),
     x <= r*x - r*x*x
    })
  )
 select new Set[]
  {
   t[j*columns + i] <= total*divider
  }

OpenCL/Cloo

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

Ссылки

1. "Leaping into Lyapunov Space", by A. K. Dewdney, Scientific American, Sept. 1991, pp. 178-180
2. Tomas Petricek. Accelerator and F# (II.): The Game of Life on GPU
3. Последняя версия исходного кода к статье на bitbucket

Дополнение по производительности вычислений под Mono

Проверил тот же код в тех условиях на Mono 2.10.2, тут все очень плохо:
LambdaTest - 2.381s
ExpressionTest - 10.506s

Итого

Использование выражений дает в "родной" среде 10-кратный выигрыш в сравнении с лямбда-функциями, и соответственно под Mono VM 4-кратное падение производительности. При оптимизации кросс-платформенного кода необходимо это учесть и предусмотреть выбор оптимальной стратегии вычислений в зависимости от среды выполнения.

понедельник, 6 июня 2011 г.

Производительность вычисления формул

У нас есть решение - обработчик OLAP-запросов, для которого критичная скорость преобразования и загрузки данных в хранилище и вычисление формул в MDX-запросах. Достоверно известно ("британские ученые доказали"), что в типовом сценарии на вычисления уходит 90% времени процессора. Это и есть проблема.

Итак, исходная реализация: предварительно разбираем выражение и строим синтаксическое дерево (AST, но в своей объектной модели). Далее при выполнении кода просто просим дерево посчитать значение выражения для текущего контекста выполнения (контекст включает текущую строку данных и область видимости блоков агрегации данных, например в матричной группировке).

Варианты решения

Первая попытка решения - вместо собственного AST формируем функцию вычисления значения, аргументом которой является контекст. Например есть выражение:
(GetRecordValue(ctx,2) * 1024 + GetRecordValue(ctx,3))/3600/(Max(1, GetRecordValue(ctx,4))

Код построения функции даже проще кода построения AST. К сожалению, на каждом этапе построения функции приходится формировать лямбда функцию с аргументом контекста, что приводит к следующей результирующей функции:

ctx5 => (ctx4 => (ctx3 => (ctx2 => (ctx => 
    GetRecordValue(ctx,2))(ctx2) * (ctx => 1024)(ctx2))(ctx3)
    + (ctx2 => (ctx => GetRecordValue(ctx,2))(ctx2))(ctx3))(ctx4) / (ctx4 => 3600))(ctx5)
  / (ctx2 => Max(ctx2 => 1, (ctx => GetRecordValue(ctx,4)(ctx2)))(ctx5)


Кроме того, для каждой лямбда функции (точнее для ее замыкания) создается класс. Несмотря на это, результат - примерно двукратный рост скорости - хорош, но недостаточно. Хотелось чтобы скорость выполнения была такая же как и для кода написанного на C#.

Выражения, появившиеся в .NET 3.5 - это аналог исходного решения, но если метод Compile использует IL Emit, можно ожидать существенно лучший результат.

Для проверки решения на практике реализуем два парсера - первый возвращает лямбда-функцию вычисления, второй - Expression.

NParsec

Проще всего сделать парсер с помощью библиотеки NParsec (последовательный порт parsec => jparsec => nparsec).

Обобщенный код парсера занимает примерно 30 строк наглядного кода:
private static Parser<T> CreateParser<T>(Func<string, T> getVar,
 IDictionary<char,Map<T,T,T>> binaryOps, Map<string,T> parseValue, Map<T,T> unaryMinus)
{
 var patCharExt = Patterns.IsChar(Char.IsLetter).Or(Patterns.IsChar('_'));
 var patVar = patCharExt.Seq(Patterns.InRange('0', '9').Or(patCharExt).Many());
 var scannerVar = Scanners.IsPattern(patVar, "var");

 var lexerVal = Lexers.Lex(scannerVar, Tokenizers.ForString);

 var operators = Terms.GetOperatorsInstance("+", "-", "*", "/", "(", ")", "^");
 var ignored = Scanners.IsWhitespaces().Many_();
 var lexeme = Lexers.Lexeme(ignored, operators.Lexer | Lexers.LexDecimal() | lexerVal).FollowedBy(Parsers.Eof());

 var pPlus = GetOperator(operators, "+", binaryOps['+']);
 var pMinus = GetOperator(operators, "-", binaryOps['-']);
 var pMul = GetOperator(operators, "*", binaryOps['*']);
 var pDiv = GetOperator(operators, "/", binaryOps['/']);
 var pPower = GetOperator(operators, "^", binaryOps['^']);
 var pNeg = GetOperator(operators, "-", unaryMinus);
 var pVar = Terms.OnString((@from, len, data) => getVar(data));

 var pNumber = Terms.OnDecimal((_, __, s) => parseValue(s));
 var lazyExpr = new Parser<T>[1];
 var pLazyExpr = Parsers.Lazy(() => lazyExpr[0]);
 var pTerm = pLazyExpr.Between(operators.GetParser("("), operators.GetParser(")")) | pNumber | pVar;
 var optable = new OperatorTable<T>()
  .Infixl(pPlus, 10).Infixl(pMinus, 10)
  .Infixl(pMul, 20).Infixl(pDiv, 20)
  .Infixl(pPower, 25).Prefix(pNeg, 30);
 var pExpr = Expressions.BuildExpressionParser(pTerm, optable);
 lazyExpr[0] = pExpr;
 return Parsers.ParseTokens(lexeme, pExpr.FollowedBy(Parsers.Eof()), "calculator");
}

private static Parser<T> GetOperator<T>(Terms ops, string op, T v)
{
 return ops.GetParser(op).Seq(Parsers.Return(v));
}


Парсеры для лямбда-функций и Expressions на основе обобщенного парсера выглядят как:
private static Parser<Func<double>> CreateLambdaParser(Func<string, Func<double>> getVar)
{
 var binaryOps = new Dictionary<char, Map<Func<double>, Func<double>, Func<double>>>
  {
   {'+', (a, b) => () => a() + b()},
   {'/', (a, b) => () => a()/b()},
   {'-', (a, b) => () => a() - b()},
   {'*', (a, b) => () => a()*b()},
   {'^', (a, b) => () => Math.Pow(a(), b())}
  };
 Map<Func<double>, Func<double>> unaryMinus = v => () => -v();
 Map<string, Func<double>> parseDouble = s =>
  {
   var v = double.Parse(s);
   return () => v;
  };

 return CreateParser(getVar, binaryOps, parseDouble, unaryMinus);
}

private static Parser<Expression> CreateExpressionParser(Func<string, Expression> getVar)
{
 var binaryOps = new Dictionary<char, Map<Expression, Expression, Expression>>
  {
   {'+', Expression.Add},
   {'/', Expression.Divide},
   {'-', Expression.Subtract},
   {'*', Expression.Multiply},
   {'^', (a,b) => Expression.Call(typeof(Math).GetMethod("Pow"), a, b)}
  };
 Map<string, Expression> parseDouble = s => Expression.Constant(double.Parse(s));

 return CreateParser(getVar, binaryOps, parseDouble, Expression.Negate);
}

Ну и собственно:

Результаты замера производительности

Для замера скорости подойдет полином 20-й степени вида ((a1+1)*a2+1)*a3 + 1... Результат - примерно 200 кратное превосходство Expression - 120 секунд против 600 мс. Результат подозрительный, но причина в том что переменные a1..a20 - это по сути константы, так как компилятор выражений, обнаружив что в правой части выражения стоит константа, вероятно подставил значения в AST и произвел оптимизацию.

Для получения достоверного результата я заменил константу на вызов функции и получил вполне достоверный результат 5.58c для лямбда-функций против 1.17с для выражений. А при включенной оптимизации компилятора C# - 2.27с против 0.26с. В результате имеем 10-кратный выигрыш при соизмеримой сложности кода.

Исходный код и тесты доступны по в репозитарии на bitbucket

понедельник, 21 февраля 2011 г.

Решение на CLinq

Интересная библиотека - Continuous Linq. Позволяет сделать "наблюдаемую" выборку, используя Linq и соответствующие методы-расширения.

Я попробовал решить исходную задачу с помощью следующего кода (правда пришлось заменить самописные ObservableCollection и сопутствующие классы на те что включены в сборку WindowsBase от третьего фреймворка):
var coll = new ContinuousCollection<Page>();

var res = 
 coll.Select(p => new Rectangle(Point.Empty, p.Size))
 .Concat(coll.Select(p => p.PageHeader.Rect))
 .Concat(coll.SelectMany(p => p.Items).Select(block => block.Rect))
 .Concat(coll.SelectMany(p => p.Items).SelectMany(b => b.Items).Select(t => t.Rect))
 ;

NotifyCollectionChangedEventHandler h = (sender, args) =>
 {
  if (args.OldItems != null)
   foreach (var oldItem in args.OldItems)
   {
    invalidateHandler((Rectangle) oldItem);
   }
  if (args.NewItems != null)
   foreach (var newItem in args.NewItems)
   {
    invalidateHandler((Rectangle) newItem);
   }
 };

res.CollectionChanged += h;

coll.Add(page);
return new DisposeDelegate(() => res.CollectionChanged -= h);

Стандартный набор тестов проходит. Несколько тестов падает, но это непринципиально (проблема момента начала наблюдения).

Интересно, совпадают ли мощности у CLinq и моего решения в плане отслеживания изменений, то есть все ли задачи доступные CLinq решаются механизмом подписки. Если это так, то можно сделать билдер на тех же LINQ расширениях. Другая задача - это генерация последовательности, аналогичной CLinq.