Что касается картинок. Я написал программу и запускал ее в учебном классе на 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-1802. Tomas Petricek. Accelerator and F# (II.): The Game of Life on GPU
3. Последняя версия исходного кода к статье на bitbucket
Комментариев нет:
Отправить комментарий