Re[25]: Технология .Net уходит в небытиё
От: alex_public  
Дата: 25.01.19 02:56
Оценка: 1 (1)
Здравствуйте, 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>
  Картинка
[url=http://i.piccy.info/i9/d9a07d64e6ed3d0808a113c7a8126118/1547766126/63038/1203760/blur_800.jpg]Image: blur_800.jpg[/URL][url=http://i.piccy.info/a3/2019-01-17-23-02/i9-12906809/800x510-r/i.gif]Image: i.gif[/URL]

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). Но опять же это всё игры с другим алгоритмом, который нельзя сравнивать с оригинальными данными.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.