Информация об изменениях

Сообщение Re[27]: Технология .Net уходит в небытиё от 25.01.2019 21:04

Изменено 25.01.2019 21:57 alexzzzz

Re[27]: Технология .Net уходит в небытиё
Здравствуйте, alex_public, Вы писали:

A>>- мой вариант C#: 4,6с

_>Похоже на правду, хотя и чуть подправленный у тебя там алгоритм (оригинальный будет всё же порядка 5 секунд). Ну и надо отмечать, что это unsafe код, а safe вариант работает больше 10 секунд.

Что ты к unsafe привязался? Safe-вариант C++ работает больше 20 секунд, если таковым считать отладочный билд с разными проверками.

A>>- похожий C# в Unity: 2,6с

_>А вот тут у тебя уже совсем другой алгоритм (более простой для SIMD оптимизации). И если запустить его аналог на C++ (см. предыдущее сообщение), то будут совсем другие результаты (и на моей машине и на твоей).

Алгоритм там везде один и тот же: перебрать все ячейки и каждую неравную FFFFFF заменить на среднее значение восьми её соседей. Идея сравнивать, как себя ведёт одинаковый до буквы код в разных языках, мне непонятна. Есть входные данные и есть представление, как должны выглядеть выходные. Берёшь в руки конкретный язык с его инструментами и с их помощью трансформируешь.

Вот · на Яве подошёл к проблеме кардинальнее, принципиально изменив порядок действий. И честно говоря, правильно сделал. Задача любой программы ― трансформировать одни данные в другие данные. Если программа работает правильно и быстро, каким образом она этого достигает, никого парить не должно. Можно лишь завидовать.

По оптимизациям, там в результате какая-то длиннющая лапша из SSE и AVX. Ни малейшего представления, что вообще происходит. Но если исходный код цикла немного помассировать, можно получить ещё два варианта: простейший тупой на «обычных» регистрах (буквально десяток операций) и ещё один, по сути как предыдущий, но на SSE-регистрах. Эти два варианта отличались от лучшего на несколько десятых секунды и признаков векторизации в них не замечено. (Если конечно мне показывали правильный дизасм).

_>Для gcc и clang любых версий и под любыми ОС правильными опциями для подобных тестов будут: -std=c++17 -march=native -O3.


Core i5-2500K @4,5ГГц / DDR3 1333 МГц

  -gcc: 6,0c

H:\Users\Alex\Desktop\CPP>g++ --version
g++ (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

H:\Users\Alex\Desktop\CPP>g++ test.cpp -o test.exe -std=c++17 -march=native -O3

H:\Users\Alex\Desktop\CPP>test
Calculation time: 6.063s. image[12345]=14371058


  -clang: 6,0c

h:\Users\Alex\Desktop\CPP>clang++ --version
clang version 7.0.1 (tags/RELEASE_701/final)
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: H:\Program Files\LLVM\bin

h:\Users\Alex\Desktop\CPP>clang++ test.cpp -o test.exe -std=c++17 -march=native -O3
test-195d14.o : warning LNK4217: locally defined symbol __std_terminate imported in function "int `int __cdecl Test(class std::vector<int,class std::allocator<int> > &,int)'::`1'::dtor$36" (?dtor$36@?0??Test@@YAHAEAV?$vector@HV?$allocator@H@std@@@std@@H@Z@4HA)
test-195d14.o : warning LNK4217: locally defined symbol _CxxThrowException imported in function "class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::operator<<<struct std::char_traits<char> >(class std::basic_ostream<char,struct std::char_traits<char> > &,char const *)" (??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z)

h:\Users\Alex\Desktop\CPP>test
Calculation time: 6.065s. image[12345]=14371058


— msvc: 5,2с (компилировал в Студии, Favor Speed (/Ox), какие были остальные ключи — не поручусь)
— icc: 4,6с
Интеловский сегодня скачал на посмотреть. Он сам интегрировался в Студию, код компилировался там же с ключом -O3; между -O1 и -O3 разница была явно заметна в пользу -O3. За остальные не поручусь. Разных тонких настроек там до чёрта, и хз надо их трогать или нет.

_>Ну пока что из принципиальных различий я вижу замену if'а на некий math.select — это было обязательно делать? Неужели такая базовая операция как if запрещена? Или это ты сделал не вынужденно, а для нечестной оптимизации (см. ниже)?


Чего не честный-то? Я не понимаю логику. Если у тебя в С++ ?: работает быстрее if, зачем искусственно замедлять собственный код? Не всё ли равно, с какой скоростью в каком-то условном Коболе будет работать оператор ?: и есть ли он там вообще. Мне лично плевать. Надо сделать быстрее ― берёшь и делаешь быстрее, без оглядки на посторонних.

Конкретно math.select — интринзик, если может, мапится в blendvps или типа того, если не может, то
/// <summary>Returns b if c is true, a otherwise.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int select(int a, int b, bool c)    { return c ? b : a; }


_>Ну и главное по этому твоему коду (который HPC#): это другой алгоритм (более толерантный к SIMD) и если ты хочешь использовать его именно в таком виде (тот if в моём тесте там совсем не просто так был, а как одна из сильно напрягающих SIMD оптимзиацию конструкций), то тогда уж надо и у других языков поменять.


Я обеими руками за то, чтобы на всех языках писать так, как наиболее эффективно писать в этих языках. Если вариант на C++ можно сделать в несколько раз быстрее, его надо сделать в несколько раз быстрее. Будет сравнение не сферических коней, а настоящего кода, и по нему можно будет ориентироваться.

Кстати, у меня замена if на ?: дала нулевой эффект. Проверил на всех четырёх компиляторах. Если и есть изменения, то в сотых долях секунды.
А, не, это я дебил. Воткнул ?: внутрь if. Перепроверил:

— gcc: 2,7 вместо 6,0
— clang: 6,1 вместо 6,0
— msvc: 5,4 вместо 5,2
— icc: 4,6 ― без изменений

  На всякий случай код
#include <iostream>
#include <vector>
#include <chrono>

using namespace std;

constexpr int COUNT=1000;

int Test(vector<int32_t>& image, int height)
{
    auto start=chrono::high_resolution_clock::now();
    auto buf=image;
    const int width=image.size()/height;
    for(int n=0; n<COUNT; n++){
        for(int y=1; y<height-1; y++){
            const auto s=image.data()+y*width;
            const auto d=buf.data()+y*width;
            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];
            }
        }
        for(int y=1; y<height-1; y++){
            const auto s=buf.data()+y*width;
            const auto d=image.data()+y*width;
            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];
            }
        }
    }
    return chrono::duration_cast<chrono::milliseconds>(chrono::high_resolution_clock::now()-start).count();
}

int main()
{
    constexpr int width=1600, height=900;
    vector<int32_t> image(width*height);
    //начальная инициализация для получения такой же забавной картинки, как и в браузере
    for(int i=0; i<width; i++) image[i]=0xffffff;
    for(int i=0; i<width; i++) image[(height-1)*width+i]=0xffffff;
    for(int i=0; i<height; i++) image[width*i]=0xffffff;
    for(int i=0; i<height; i++) image[width*i+width-1]=0xffffff;
    for(int i=width/2-100; i<width/2+100; i++) image[width*height/2+i]=0xffffff;
    for(int i=height/2-100; i<height/2+100; i++) image[width*i+width/2]=0xffffff;
    
    cout<<"Calculation time:\t"<<Test(image, height)/1000.0<<"s.\timage[12345]="<<image[12345]<<endl;
    
    return 0;
}

_>Кроме этого, я вижу что работа идёт с некими NativeArray, а не стандартными .net массивами. Хотя это естественно не минус, но код всё же явно требует ручного портирования, а не просто некой перекомпиляции.

NativeArray ― это struct-обёртка над памятью, типа Span<T>/Memory<T>, но со встроенными защитными механизмами раннего отлова потенциальных race conditions. Например, если в одном потоке такой массив помечен атрибутом только для чтения, а в другом разрешён для записи, и если потоки не были правильно формально синхронизированы, то NativeArray гарантирует, что код мгновенно упадёт с объяснением, в каком месте проблема, в чём заключается и как исправить. Даже если потоки разошлись по времени и в данный момент проблемы как бы нет; достаточно лишь теоретической вероятности гонки. Или, допустим, потоки могут читать/писать в один и тот же массив, но каждый только в свою область; если поток по ошибке заденет чужую, то сразу стоп машина и красный флаг. Реально полезная штука.

В принципе, числодробилка может принимать на вход обычные управляемые массивы; если хочет, делать себе поверх них NativeArray<T>, безопасно работать с ними во множестве потоков, а результаты будут отображаться в исходные массивы. Пока не пробовал, но по идее можно работать и со Span<T>/Memory<T>, так даже универсальнее, чем только с массивами.
Re[27]: Технология .Net уходит в небытиё
Здравствуйте, alex_public, Вы писали:

A>>- мой вариант C#: 4,6с

_>Похоже на правду, хотя и чуть подправленный у тебя там алгоритм (оригинальный будет всё же порядка 5 секунд). Ну и надо отмечать, что это unsafe код, а safe вариант работает больше 10 секунд.

Что ты к unsafe привязался? Safe-вариант C++ работает больше 20 секунд, если таковым считать отладочный билд с разными проверками.

A>>- похожий C# в Unity: 2,6с

_>А вот тут у тебя уже совсем другой алгоритм (более простой для SIMD оптимизации). И если запустить его аналог на C++ (см. предыдущее сообщение), то будут совсем другие результаты (и на моей машине и на твоей).

Алгоритм там везде один и тот же: перебрать все ячейки и каждую неравную FFFFFF заменить на среднее значение восьми её соседей. Идея сравнивать, как себя ведёт одинаковый до буквы код в разных языках, мне непонятна. Есть входные данные и есть представление, как должны выглядеть выходные. Берёшь в руки конкретный язык с его инструментами и с их помощью трансформируешь.

Вот · на Яве подошёл к проблеме кардинальнее, принципиально изменив порядок действий. И честно говоря, правильно сделал. Задача любой программы ― трансформировать одни данные в другие данные. Если программа работает правильно и быстро, каким образом она этого достигает, никого парить не должно. Можно лишь завидовать.

По оптимизациям, там в результате какая-то длиннющая лапша из SSE и AVX. Ни малейшего представления, что вообще происходит. Но если исходный код цикла немного помассировать, можно получить ещё два варианта: простейший тупой на «обычных» регистрах (буквально десяток операций) и ещё один, по сути как предыдущий, но на SSE-регистрах. Эти два варианта отличались от лучшего на несколько десятых секунды и признаков векторизации в них не замечено. (Если конечно мне показывали правильный дизасм).

_>Для gcc и clang любых версий и под любыми ОС правильными опциями для подобных тестов будут: -std=c++17 -march=native -O3.


Core i5-2500K @4,5ГГц / DDR3 1333 МГц

  -gcc: 6,0c

H:\Users\Alex\Desktop\CPP>g++ --version
g++ (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

H:\Users\Alex\Desktop\CPP>g++ test.cpp -o test.exe -std=c++17 -march=native -O3

H:\Users\Alex\Desktop\CPP>test
Calculation time: 6.063s. image[12345]=14371058


  -clang: 6,0c

h:\Users\Alex\Desktop\CPP>clang++ --version
clang version 7.0.1 (tags/RELEASE_701/final)
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: H:\Program Files\LLVM\bin

h:\Users\Alex\Desktop\CPP>clang++ test.cpp -o test.exe -std=c++17 -march=native -O3
test-195d14.o : warning LNK4217: locally defined symbol __std_terminate imported in function "int `int __cdecl Test(class std::vector<int,class std::allocator<int> > &,int)'::`1'::dtor$36" (?dtor$36@?0??Test@@YAHAEAV?$vector@HV?$allocator@H@std@@@std@@H@Z@4HA)
test-195d14.o : warning LNK4217: locally defined symbol _CxxThrowException imported in function "class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl std::operator<<<struct std::char_traits<char> >(class std::basic_ostream<char,struct std::char_traits<char> > &,char const *)" (??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z)

h:\Users\Alex\Desktop\CPP>test
Calculation time: 6.065s. image[12345]=14371058


— msvc: 5,2с (компилировал в Студии, Favor Speed (/Ox), какие были остальные ключи — не поручусь)
— icc: 4,6с
Интеловский сегодня скачал на посмотреть. Он сам интегрировался в Студию, код компилировался там же с ключом -O3; между -O1 и -O3 разница была явно заметна в пользу -O3. За остальные не поручусь. Разных тонких настроек там до чёрта, и хз надо их трогать или нет.

_>Ну пока что из принципиальных различий я вижу замену if'а на некий math.select — это было обязательно делать? Неужели такая базовая операция как if запрещена? Или это ты сделал не вынужденно, а для нечестной оптимизации (см. ниже)?


Чего не честный-то? Я не понимаю логику. Если у тебя в С++ ?: работает быстрее if, зачем искусственно замедлять собственный код? Не всё ли равно, с какой скоростью в каком-то условном Коболе будет работать оператор ?: и есть ли он там вообще. Мне лично плевать. Надо сделать быстрее ― берёшь и делаешь быстрее, без оглядки на посторонних.

Конкретно math.select — интринзик, если может, мапится в blendvps или типа того, если не может, то
/// <summary>Returns b if c is true, a otherwise.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int select(int a, int b, bool c)    { return c ? b : a; }


_>Ну и главное по этому твоему коду (который HPC#): это другой алгоритм (более толерантный к SIMD) и если ты хочешь использовать его именно в таком виде (тот if в моём тесте там совсем не просто так был, а как одна из сильно напрягающих SIMD оптимзиацию конструкций), то тогда уж надо и у других языков поменять.


Я обеими руками за то, чтобы на всех языках писать так, как наиболее эффективно писать в этих языках. Если вариант на C++ можно сделать в несколько раз быстрее, его надо сделать в несколько раз быстрее. Будет сравнение не сферических коней, а настоящего кода, и по нему можно будет ориентироваться.

Кстати, у меня замена if на ?: дала нулевой эффект. Проверил на всех четырёх компиляторах. Если и есть изменения, то в сотых долях секунды.
А, не, это я дебил. Воткнул ?: внутрь if. Перепроверил:

— gcc: 2,7 вместо 6,0
— clang: 6,1 вместо 6,0
— msvc: 5,4 вместо 5,2
— icc: 4,6 ― без изменений

  На всякий случай код
#include <iostream>
#include <vector>
#include <chrono>

using namespace std;

constexpr int COUNT=1000;

int Test(vector<int32_t>& image, int height)
{
    auto start=chrono::high_resolution_clock::now();
    auto buf=image;
    const int width=image.size()/height;
    for(int n=0; n<COUNT; n++){
        for(int y=1; y<height-1; y++){
            const auto s=image.data()+y*width;
            const auto d=buf.data()+y*width;
            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];
            }
        }
        for(int y=1; y<height-1; y++){
            const auto s=buf.data()+y*width;
            const auto d=image.data()+y*width;
            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];
            }
        }
    }
    return chrono::duration_cast<chrono::milliseconds>(chrono::high_resolution_clock::now()-start).count();
}

int main()
{
    constexpr int width=1600, height=900;
    vector<int32_t> image(width*height);
    //начальная инициализация для получения такой же забавной картинки, как и в браузере
    for(int i=0; i<width; i++) image[i]=0xffffff;
    for(int i=0; i<width; i++) image[(height-1)*width+i]=0xffffff;
    for(int i=0; i<height; i++) image[width*i]=0xffffff;
    for(int i=0; i<height; i++) image[width*i+width-1]=0xffffff;
    for(int i=width/2-100; i<width/2+100; i++) image[width*height/2+i]=0xffffff;
    for(int i=height/2-100; i<height/2+100; i++) image[width*i+width/2]=0xffffff;
    
    cout<<"Calculation time:\t"<<Test(image, height)/1000.0<<"s.\timage[12345]="<<image[12345]<<endl;
    
    return 0;
}

_>Кроме этого, я вижу что работа идёт с некими NativeArray, а не стандартными .net массивами. Хотя это естественно не минус, но код всё же явно требует ручного портирования, а не просто некой перекомпиляции.

NativeArray ― это struct-обёртка над памятью, типа Span<T>/Memory<T>, но со встроенными защитными механизмами раннего отлова потенциальных race conditions. Например, если в одном потоке такой массив помечен атрибутом только для чтения, а в другом разрешён для записи, и если потоки не были правильно формально синхронизированы, то NativeArray гарантирует, что код мгновенно упадёт с объяснением, в каком месте проблема, в чём заключается и как исправить. Даже если потоки разошлись по времени и в данный момент проблемы как бы нет; достаточно лишь теоретической вероятности гонки. Или, допустим, потоки могут читать/писать в один и тот же массив, но каждый только в свою область; если поток по ошибке заденет чужую, то сразу стоп машина и красный флаг. Реально полезная штука.

В принципе, числодробилка может принимать на вход обычные управляемые массивы; если хочет, делать себе поверх них NativeArray<T>, безопасно работать с ними во множестве потоков, а результаты будут отображаться в исходные массивы. Пока не пробовал, но по идее можно работать и со Span<T>/Memory<T>, так даже универсальнее, чем только с массивами, но менее безопасно, чем с NativeArray<T>.