Здравствуйте, alexzzzz, Вы писали:
>> Ну и 5,9 с — это у тебя какая-то невероятная цифра, даже для убогого msvc.
A>У меня нет AVX-2 и AVX-512. Возможно, они как-то сильно ускоряют конкретно этот тест. Моему процессору конечно 7 лет, но в популярных тестах в два раза в однопотоке его пока никто обогнать не может.
Ну, чтобы получить результаты как у меня или "·" всё же необходим процессор с AVX2 (именно с этим кодом — см. конец сообщения). С AVX-512 должно быть ещё в два раз быстрее, но у меня сейчас такого под рукой нет.
>> А вообще не вижу никакого смысла в использование этого убожества в случае потребности в производительном коде. Даже халявный gcc (MinGW) на голову быстрее и стабильнее, я уже не говорю о компиляторе от Intel...
A>То ли msvc недостаточно плох, то ли icc недостаточно хорош. По наблюдениям, в игрушках последний непопулярен, хотя потребности в производительном коде хоть отбавляй. Может, он на не интелах тормозит, не знаю. Больше используются msvc/clang/gcc.
Да геймдев — это вообще отдельный мирок в сообществе C++, который характеризуется весьма сомнительными особенностями (любовь к VS, любовь к велосипедам и ещё много чего), которые в других областях оставили в прошлом ещё десятилетие назад. На эту тему недавно было большое обсуждение. Точнее был некий коллективный вопль из мира геймдева и потом разбор его ошибочности во многих местах. Например тут
https://habr.com/ru/post/435036/ можно увидеть довольно адекватное мнение — рекомендую к прочтению.
A>>>У меня C# под обычным .Net показывает 3,3с.
_>>А что за C# код то? Если брать каноничный (safe) код, то он исполняется у меня порядка 10,8 секунды. Если же взять unsafe код с голыми указателями и ещё страшными (внешне) извращениями предложенными когда-то Sinix, то это будет около 5 секунд.
A>А в тех старых тестах не было случайно 1920x1080x500? Откуда-то я эти 500 итераций взял и помню, что менял в коде разрешение с 1920x1080 на 1600x900.
Да, именно так и было. А потом у меня появился тест с WebAssembly в браузерном окне (с визуализацией) и там было неудобно наблюдать, так что я уменьшил разрешение и увеличил количество итераций.
A>Если брать каноничный (safe) код, со стороны C/С++ кто будет выступать? Unsafe дан Шарпу от рождения, в отличие, например, от дженериков. Куда уж каноничнее.
Ну это весьма спорный вопрос... И как раз чтобы не провоцировать ненужных споров, я всегда указываю в тесте оба варианта — это получается абсолютно честно. Собственно в твоём коде тоже оба варианта есть и оба нормальные (а не то, чем тут в последнем сообщение "·" развлекался), так что единственная моя претензия к твоим результатам в том, как ты это озвучиваешь: всё же мне кажется правильно уточнять какой вариант кода (safe/unsafe) используется.
A>Обновил C#/Net версию: 5,1с. Звёздочки постарался скрыть, чтобы никого не пугали.
У меня твой вариант C# unsafe кода выполняется 4,4 секунды, но по сравнению с вариантом от Sinix он имеет алгоритмические (т.е. которые тогда надо в тестах для других языков вставлять), а не архитектурные/компиляторные улучшения. Так что пожалуй не буду заменять твоим вариантом код в моём наборе, хоть он и чуть быстрее. Но в целом это укладывающийся в общую схему результат.
_>>P.S. Глянул в гугле что такое HPC# — это компиляция некого подмножества C# в C++ и использование далее gcc, правильно? Если так, то ничего удивительного в таких результатах нет (можно было даже лучше)...
A>Не совсем. В Unity несколько вариантов получения финального кода из IL:
A>1. Классический, с JIT-компилятором.
A>2. IL2CPP. IL транслируется в C++, а дальше в машинный код или в JavaScript/Wasm. Изначально создавалось для AOT-компиляции под 64-битную iOS, потом приспособлено для WebGL. Про ускорение там речь не идёт. Если получается, кто как приятный бонус.
A>3. Burst-компилятор/транслятор. Транслирует IL, удовлетворяющий некоторым ограничениям, в LLVM IR, LLVM из него выдаёт финальный код. Вот оно делается ради скорости. C++ в цепочке не участвует.
Ну собственно не суть, используется оптимизатор от gcc или от llvm. Собственно второй вариант более правильный, т.к. C++ в данной схеме явно лишний. Однако оптимизаторы всё равно всё те же. И подход тот же: по сути речь идёт о нативном коде, наверняка без всяких там глупостей из .net типа рефлексии и т.п. Кстати, а память то там выделяется хоть сборщиком мусора или и его выкинули? )))
A>Ограничения HPC# на практике — фигня. Ты пишешь свой C# как обычно, только критичные к скорости части оформляешь и выделяешь так, чтобы они соответствовали ограничениям HPC#, и чтобы код, что оттуда вызывается, тоже им соответствовал. Делаешь пару взмахов руками и оно ускоряется в пару-тройку раз. Фигня, потому что критичный код всё равно примерно так бы и выглядел безо всяких Бёрстов.
A>
A>Билд: http://dl.dropbox.com/s/p199vcuaqasdhqm/HPC%23.zip (19Мб)
A>При запуске некоторое время висит — гоняет тест 10 раз, результаты пишет в текущую папку в файл results.txt. Как очнётся, показывает исходную картинку и при удерживании пробела делает её обработку.
A>Код выглядит так. Другого кода в проекте нет.
A> | Скрытый текст |
| using System.Diagnostics;
A>using System.IO;
A>using Unity.Burst;
A>using Unity.Collections;
A>using Unity.Collections.LowLevel.Unsafe;
A>using Unity.Jobs;
A>using Unity.Mathematics;
A>using UnityEngine;
A>using UnityEngine.UI;
A>public class Benchmark : MonoBehaviour
A>{
A> private const int COUNT = 1000;
A> private const int WIDTH = 1600, HEIGHT = 900;
A> private Texture2D texture;
A> private NativeArray<int> image;
A> private NativeArray<int> buffer;
A> private NativeArray<int> results;
A> private ProcessJob processJob;
A> public void Start()
A> {
A> image = new NativeArray<int>(WIDTH * HEIGHT, Allocator.TempJob);
A> buffer = new NativeArray<int>(image.Length, Allocator.TempJob);
A> results = new NativeArray<int>(length: 1, Allocator.TempJob);
A> processJob = new ProcessJob
A> {
A> image = image,
A> buffer = buffer,
A> count = COUNT,
A> height = HEIGHT,
A> results = results,
A> };
A> var filename = Path.Combine(Directory.GetCurrentDirectory(), "results.txt");
A> using (var writer = File.AppendText(filename))
A> {
A> var sw = new Stopwatch();
A> for (int i = 0; i < 10; i++)
A> {
A> LoadImage(image, WIDTH, HEIGHT);
A> sw.Restart();
A> processJob.Run();
A> sw.Stop();
A> writer.Write($"{i}. {sw.ElapsedMilliseconds} ms, image[12345]={results[0]}\n");
A> writer.Flush();
A> }
A> writer.Write("done\n\n");
A> }
A> LoadImage(image, WIDTH, HEIGHT);
A> processJob.count = 1;
A> texture = new Texture2D(WIDTH, HEIGHT, TextureFormat.RGBA32, false);
A> GetComponent<RawImage>().texture = texture;
A> image.CopyTo(texture.GetRawTextureData<int>());
A> texture.Apply();
A> }
A> private void OnDestroy()
A> {
A> image.Dispose();
A> buffer.Dispose();
A> results.Dispose();
A> }
A> private JobHandle handle = default;
A> private void Update()
A> {
A> handle.Complete();
A> if (Input.GetKey(KeyCode.Space))
A> {
A> image.CopyTo(texture.GetRawTextureData<int>());
A> texture.Apply();
A> handle = processJob.Schedule();
A> JobHandle.ScheduleBatchedJobs();
A> }
A> }
A> private static void LoadImage(NativeArray<int> image, int width, int height)
A> {
A> for (int i = 0; i < image.Length; i++) image[i] = 0;
A> for (int i = 0; i < width; i++) image[i] = 0xffffff;
A> for (int i = 0; i < width; i++) image[(height - 1) * width + i] = 0xffffff;
A> for (int i = 0; i < height; i++) image[width * i] = 0xffffff;
A> for (int i = 0; i < height; i++) image[width * i + width - 1] = 0xffffff;
A> for (int i = width / 2 - 100; i < width / 2 + 100; i++) image[width * height / 2 + i] = 0xffffff;
A> for (int i = height / 2 - 100; i < height / 2 + 100; i++) image[width * i + width / 2] = 0xffffff;
A> }
A> [BurstCompile(CompileSynchronously = true)]
A> internal unsafe struct ProcessJob : IJob
A> {
A> public NativeArray<int> image;
A> public NativeArray<int> buffer;
A> public int height;
A> public int count;
A> public NativeArray<int> results;
A> public void Execute()
A> {
A> image.CopyTo(buffer);
A> var imagePtr = (int*)image.GetUnsafePtr();
A> var bufferPtr = (int*)buffer.GetUnsafePtr();
A> int width = image.Length / height;
A> for (int i = 0; i < count; i++)
A> {
A> ProcessImage(imagePtr, bufferPtr, width, height);
A> ProcessImage(bufferPtr, imagePtr, width, height);
A> }
A> results[0] = image[12345];
A> }
A> private static void ProcessImage(int* input, int* output, int width, int height)
A> {
A> for (int y = 1; y < height - 1; y++)
A> {
A> var s = input + y * width + 1;
A> var d = output + y * width;
A> for (int x = 1; x < width - 1; x++)
A> {
A> int sum = (s[-width - 1] + s[-width] + s[-width + 1] +
A> s[-1] + s[1] +
A> s[width - 1] + s[width] + s[width + 1]) >> 3;
A> d[x] = math.select(d[x], sum, *s++ != 0xffffff);
A> }
A> }
A> }
A> }
A>}
|
| |
Ну пока что из принципиальных различий я вижу замену if'а на некий math.select — это было обязательно делать? Неужели такая базовая операция как if запрещена? Или это ты сделал не вынужденно, а для нечестной оптимизации (см. ниже)?
Кроме этого, я вижу что работа идёт с некими NativeArray, а не стандартными .net массивами. Хотя это естественно не минус, но код всё же явно требует ручного портирования, а не просто некой перекомпиляции.
Ну и главное по этому твоему коду (который HPC#): это другой алгоритм (более толерантный к SIMD) и если ты хочешь использовать его именно в таком виде (тот if в моём тесте там совсем не просто так был, а как одна из сильно напрягающих SIMD оптимзиацию конструкций), то тогда уж надо и у других языков поменять. Например для C++ замена вот этого куска моего кода:
for(int x=1; x<width-1; x++) if(s[x]!=0xffffff)
d[x]=(s[-width+x]+s[x-1]+s[-width+x-1]+s[-width+x+1]+s[width+x-1]+s[width+x]+s[x+1]+s[width+x+1])>>3;
на такой кусок:
for(int x=1; x<width-1; x++){
int sum=(s[-width+x]+s[x-1]+s[-width+x-1]+s[-width+x+1]+s[width+x-1]+s[width+x]+s[x+1]+s[width+x+1])>>3;
d[x]=s[x]!=0xffffff?sum:d[x];
}
даст ускорение работы в 1,5 раза на моём процессоре (т.е. у меня оно будет считаться уже меньше 1,5 секунд). Более того, подобное изменение алгоритма позволит получить AVX ускорение на твоём процессоре (думаю должно получиться порядка 2,2-2,3 секунд у тебя с gcc). Но опять же это всё игры с другим алгоритмом, который нельзя сравнивать с оригинальными данными.