Comments 67
Замеры производились с помощью System.Diagnostics.Stopwatch, который точнее DateTime, потому что реализован поверх PerformanceCounter.
Why not BenchmarkDotNet?
Более того, итератор гораздо более семантически «чист», чем индекс массива.
Запутаться может только неосилятор, но никак не компилятор.
Чем меньше вариантов и чем лучше вы конкретизируете что вы хотите выразить в коде, тем больше шансов на оптимизацию. «Запутаться» тут просто фигура речи. Давайте рассмотрим классический пример:
*dst++ = *src++; // Copy buffer
Даже в таком простейшем случае компилятор не сможет заменить код на оптимизированный std::memcpy, если не будет уверен что эти указатели не перекрываются. В случае с индексами и массивами у вас больше шансов:
dst[n++] = src[m++]; // Copy buffer
Чего уж говорить про более сложные случаи.
int * restrict dst = ...;
int * restrict src = ...;
*dst = *src;
++dst;
++src;
Кстати, про memcpy. У меня были реальные ситуации, когда копирование в цикле было несколько эффективнее memcpy. Я сильно не вдавался, скорее всего из-за того, что memcpy работает на уровне байт, а в цикле копировалось по 4-8 байт и профит был за счёт меньшего числа итераций. Экономия на инкрементах xD.
И ещё раз про memcpy. Вы точно не перепутали его с memmove? memcpy это просто цикл, на инлайн ассемблере (по крайней мере так было в MSVC лет пять назад), там нет требований по непересекаемости областей памяти.
скорее всего из-за того, что memcpy работает на уровне байтЭто не так. Можете сами убедиться. MSVC копирует самими широкими словами, которые есть в его распоряжении.
вы точно не перепутали его с memmoveТочно. У memmove вообще не бывает проблем с перекрытием, она для этого и предназначена.
Значит, они уже улучшили функцию, разбив цикл копирования на две части: копирование DWORD'ами (QWORD'ами) и добивание оставшегося числа байт по одному. Именно такой алгоритм у меня работал быстрее стандартного memcpy в 2005 студии, или примерно в те года.
Приведи пример, где итератор (указатель) дает худшую оптимизацию, чем индекс
Более того, итератор гораздо более семантически «чист», чем индекс массива.Речь об этом твоем утверждении
Более того, итератор гораздо более семантически «чист», чем индекс массива.Я это не утвеждал или вы непонятно объясняете о чем речь.
С точки зрения производительности, я уверен что современный компилятор построит абсолютно идентичный код и для указателей и для индексов (это в случае если код нельзя выбросить и его нужно выполнять в лоб, а нужно исполнить механически). А вот если у вас мало мальски сложный код, да еще и на шаблонах, то указатели могут помешать выкинуть неиспользуемый код или оптимизировать его для конкретного частного случая. Для демонстрации рабочего примера нужно проделать много работы, что бы вычленить минимально рабочий пример, а это не тот случай, или искать в интернете, так что ограничимся простейшим:
uint8_t buf[100];
void IndexFill()
{
for (uint_t n = 0; n < 100; n++)
buf[n] = 255;
}
void PointerFill()
{
uint8_t* bufp = buf;
for (uint_t n = 0; n < 100; n++)
*bufp++ = 255;
}
int main(int argc, char_t* argv[])
{
IndexFill();
PointerFill();
return 0;
}
VS2013 в первом случае выбрасывает вызов IndexFill() так как результат нигде не используется, а во втором случае PointerFill() честно заполняет массив, так как указатель сбивает ее с толку.
Поясню. Итератор (== указатель) по определению обозначает последовательный обход контейнера. При помощи индекса обход может быть хоть последовательный, хоть случайный, хоть какой. Т.е. если используется итератор, код сам говорит о том, что требуется пройти по всем элементам контейнера. Я не беру в расчёт говнокод, когда обращение по индексу заменяют на *(ptr+10), это уже на совести программиста пусть остаётся. Так же итераторы позволяют легко заменить тип контейнера, если это потребуется, практически не изменяя кода.
Теперь, если компилятор «тупой» (80-е годы) и процессор «тупой» (какой-нибудь DSP простенький), то никто не спорит, что указатели будут быстрее. Я сам начинал писать с PDP-11 ассемблера и очень хорошо представляю что там под капотом. Но время идет и прогресс не стоит на месте. Современные компиляторы в состоянии понять что доступ к индексам последовательный и заменять все умножения на последовательные сложения. Процессоры теперь также имеют множество блоком ALU и могут выполнять сразу много сложений и умножений за один такт, регистры становятся все шире и шире.
Сейчас предпочтительней писать более понятный код, как можно яснее выражать семантику операции и свои намерения. Не нужно заниматься ручной оптимизацией. В случае микрооптимизаций (в пределах одной функции), указатели все еще могут дать выигрыш, но в глобальной оптимизации они только мешают (если мы передаем указатели на данные и объекты из функции в функция или из одного модуля в другой), особенно в ситуации с шаблонами С++, где оптимизатор порой, просто творит чудеса.
> уже полезли итераторы, которые являются указателями только семантически.
Ну, вот, вы подтвердили моё утверждение про семантическую «чистоту» указателей.) Для итерации по массиву указатель(он же итератор) более семантически «чист» чем индекс.
Просто написать два варианта и измерить скорость — не имеет смыслаСогласен, не имеет. Просто я, примерно, видя код уже могу сказать что выигрыша не будет просто исходя из опыта. Часто приходится иметь дело с оптимизацией и все эти штуки MSVC уже знаешь.
Ну, вот, вы подтвердили моё утверждение про семантическую «чистоту» указателейЯ и не спорю. Я правда не уверен что вы имеете ввиду под «чистотой». Если более сильную абстракцию, как и я, то это хорошо только для архитектуры, но в тоже время плохо для оптимизации, где чем более специфический и конкретный будет оператор тем лучше.
Ну и на втором графике видим бОльшую стабильность, график более гладкий. Тоже плюс, наверное.)
У ABI cишки слишком много консервативных и устаревших ограничений для сохранения портабельности, которые нынче не то что не нужны, а просто ставят крест на вертикальном масштабировании большинства решений.
Можете привести пример какие такие сишные ABI?
а также отсутствие вменяемой оптимизации в случае с Сишкой на форточках
Вменяемой оптимизации по сравнению с чем?
но по уровню оптимизации сишка уступает тому же rust'у.
ну там же сам автор признал, что первая попытка была неверная и написал специально второй пост в котором ему также указали, что для получения схожего эффекта есть ключевое слово restrict.
По этому от фронтенда до фронтенда один и тот же биткод (они решили отличиться) будет оптимизирован по разному.
Да что уж там, один и тот же байткод будет отличатся от компилятора к компилятору ну и что?
Например вот это нечто пожирающее стэк без острой необходимости.
И какое отношение это имеет к "сишичке"? Изначальное утверждение было о том, что сишные ABI консервативны устаревши и ставят крест на вертикальной масштабируемости, вот я и спрашиваю какие такие сишные ABI?
С официальным гайдом по оптимизации.
Не совсем понял, какой-то компилятор генерирует код под форточки, который противоречит этому ману? Или тут упор на то, что ручная оптимизация качественней чем автоматическая?
Кстати современные (условно, примерно лет 15 последних) оптимизирующие компиляторы делают порой чудные оптимизации до которых так просто и не додумаешся. Не говоря о том, что код на "сишичке" нужно грамотно расхинтовать для началу,
Прямое, это и есть сишное ABI, — унифицированная конвенция вызова сишных функций в форточках на x64 системах.
Т.е. сишное это то, которое может использоваться в си? Сами по себе ни Си ни С++ не определяют никаких ABI. Ну и каким образом это соглашение о вызове ставит крест на вертикальной масштабируемости?
Оно не сможет эффективно использовать большую часть существующих SIMD инструкций и менять последовательности команд в различных оптимизационных целях
Так оно же мысли читать не умеет, компилятору без хинта не понятно нужно считывать все время значение по адресу или программист задумал, что значение меняться не будет и можно закешировать значение в регистре.
Оно не сможет эффективно использовать большую часть существующих SIMD инструкций и менять последовательности команд в различных оптимизационных целях…
Ну допустим пока не сможет, хотя верится слабо, зачем тогда нужен программист? )
Пример что вы привели правильно говорит что некоторые случаи в расте для оптимизитора лучше, но это не значит что на си нельзя оптимизировать чтобы было так же.
Такую же функцию, чтобы получилась 1 асм инструкция легко сделать и на си, более того многие inrinsic функции так и сделаны в системных инклудах, и принимать они могут на вход и выход любые регистры, несмотря на ABI.
Дело вот в чем, в расте просто все функции fn foo() по дефолту скрытые и не имеют привязки к ABI. Как только вы делаете функцию публичной pub extern «C» fn foo() и доступной извне, она приобретает ABI. В си же не имеют привязки статические функции, инлайновые и при использовании whole programm optimization(или и использованием профайловой оптимизации), которые не экспортируются (соответственно ABI им не нужен). Соответственно точно так же можно промаркировать интерфейсные функции торчащие наружу, а остальные скрыть и они будут оптимизироваться как угодно. Так что с ABI пример плохой.
То про что вы говорите это:
If the compiler can identify all of the call sites of a function, the compiler ignores explicit calling-convention modifiers on a function and tries to optimize the function's calling convention:
И это только один из пунктов. Собственно rust использует абсолютно те же механизмы llvm что и си.
Мне кажется, основной выигрыш в производительности достигается за счёт инлайнинга, а не оптимизации соглашения о вызовах. А для x64 нет смысла менять конвенцию — параметры и так передаются через регистры.
Есть очень много особенностей и ограничений целевых компиляторов, а также отсутствие вменяемой оптимизации в случае с Сишкой на форточках.
Как это нету? А Intel Compiler?
В llvm этот вопрос решён хоть как-то, но по уровню оптимизации сишка уступает тому же rust'у.
По приведенной ссылке:
Update Actually as dbaupp points out, this optimization example is not specific to Rust. In fact, LLVM is doing it via local > analysis (the sort I mentioned above), and clang++ produces the same results for the equivalent C++ code. In fact Rust > in general would need to preserve references because you can convert a reference to a raw pointer and compare them > for equality.
Ну и stackalloc до кучи.
Так же в CoreFX появилось много других плюшек для более эффективной работы с памятью.
Ну и это все в unsafe сделать. Страшного тут ничего нет ;)
Пример кода со всякими интересными штуками можно глянуть тут.
Ну и не использовать уже 4.5.2. Хотя бы 4.6.2, а лучше 4.7, если вам именно .Net нужен. Про BenchmarkDotNet вам уже сказали ;)
А ещё можно попробовать .Net Native или пре-компиляцию библиотек в C#.
Вот еще заинтересованным почитать Safe Systems Programming in C# and .NET.
В c# есть оптимизация при работе с массивами, если в качестве верхней границы цикла использовать не переменную length, а именно array.Length, то он выкинет проверку выхода за границы массива. Это же справедливо и для конструкции foreach.
Можете сделать соответсвующие замеры?
Спасибо, уважаемый! Очень интересная статья и полезная тема, комменты тут — просто клад для новичка)
Разве что время загрузки учитывается

.NET Managed + C unmanaged: какова цена?