В первую очередь, прошу обратить внимание на то, что эксперимент я назвал словом «пробный» – и это не случайно. Я задумал более детальное изучение особенностей разных алгоритмов, а для этого нужна помощь разных людей (хотя бы косвенная – что-то где-то запустить и сказать результат). Чтобы в будущем сложные эксперименты проходили достаточно гладко, нужно потренироваться, проверить вообще принципиальную возможность делать это подобным образом. Сообщество обычно любит пинать за ошибки в методологии исследования или организации, а мне как раз нужны были конструктивные указания на подобные ошибки. Таковые, к счастью, были.
Во-вторых, прошу меня извинить за то, что я забыл про необходимость хаотичного потока данных, из-за этого кому-то пришлось тестировать всё заново и возникла путаница в том, которую именно версию проверил очередной участник. В своей статье про подсчёт единичных битов я очень хорошо помнил о важности случайного потока входных данных, а вот здесь почему-то забыл. Бывает. Но, как я сказал выше, это первая и самая важная для меня причина проведения пробного эксперимента, чтобы меньше ошибиться в серьёзных исследованиях и заблаговременно учесть возможные конструктивные советы.
Теперь поговорим о методологии самого исследования и о том, в чём вообще необходимость тестировать какие-либо функции на скорость. И есть ли она, эта необходимость. Отсюда и начнётся философия.
Пояснение к слову «ветвление»
Другая из моих ошибок, которые я допустил, в том, что я не пояснил что значит ветвление. Обычно меня мало интересуют академические термины, потому что зачастую они не имеют отношения к реальности, а потому приходится наделять многие слова собственным смыслом.
В той статье имелось в виду именно алгоритмическое (или логическое) ветвление, которое неизбежно появляется в логике программы при вычислении логического выражения и последующих действиях в зависимости от результата этого выражения. Таким образом, когда в коде программы мы видим условие, то по логике (именно по человеческой логике) это ветвление. Почему так? Дело в том, что ветвление вообще говоря сложно определить по нормальному, когда логика алгоритма переводится сначала на ЯП, затем компилятор переводит её на машинный язык, а затем сами инструкции процессора переводятся во внутренние, скажем, RISC-инструкции. На одном этапе ветвление есть, а ну другом нет… а потом снова может появиться.
То есть вы можете думать, что ветвления в вашей программе на языке Си нет, а на самом деле оно есть, или наоборот, вы написал код с ветвлением (в логике), а компилятор придумал, как это сделать без ветвлений в коде. Пример:
u64 a, c;
u32 b;
c = a / b;
Ветвления нет. Правда? На самом деле есть (по крайней мере после компилятора VC++ 2015). Дело в том, что при так называемом «2/1»-делении (когда нечто удвоенного размера делится на нечто одинарного размера) результат может иметь как одинарный, так и двойной размер. В первом случае данная процедура деления будет выполняться с одной инструкцией div, во втором случае – с двумя. Так вот, чтобы понять то, по какой ветке пойти при таком делении, нужно рассчитать размер частного и сделать выбор, этот выбор будет одним из ветвлений перед непосредственным делением. Ещё одним выбором может быть, например, проверка на деление на ноль (хотя обычно такой проверки нет). Короче, процедура деления не сводится к обычному div, это весьма сложная процедура. А программист, глядя на эту запись, может думать, что здесь линейный код.
Второй пример. Функция максимума может выглядеть так:
i32 max1 (i32 a, i32 b) {
return a ^ ((a ^ b) & -(a < b));
}
Здесь, очевидно, нет ветвлений на архитектуре x86, наверное, не будет их и на многих других архитектурах, хотя тут есть прямое логическое сравнение двух чисел. То есть по логике ветвление здесь есть, а по коду нет. Однако замените i32 на i64 и скомпилируйте программу в режиме x86. Ветвления сразу появятся. А вот в таком коде:
i32 maxi1 (i32 a, i32 b) {
i32 d = a-b;
return b + (d&(~(d^((a^b)&(d^a))) >> SHIFT));
}
Ветвлений нет в логике и не будет в программе ни при каком фиксированном размере переменной, которую поддерживает компилятор (речь идёт об x86 при правильной компиляции). Поэтому я и называю это единственным методом поиска максимума (из мне известных), который не содержит ветвлений.
К чему это я всё рассказываю?
Основная проблема программистов, которые немного знают о классических методах оптимизации программ, но не очень глубоко в них разбирались: желание избавляться от ветвлений везде, где только можно. Во-первых, это круто и красиво, во-вторых, можно блеснуть в глазах коллег по цеху, в-третьих, прикосновение к магии завораживает, как-то поднимает самооценку и вот вроде бы не зря живешь. Но очень редко в списке причин на самом деле лежит понимание того, как быстро или как медленно будет работать новый код. Избавляясь от ветвления, программист, во-первых, может его породить, а, во-вторых, может замедлить код из-за того, что новый алгоритм более сложен по своей структуре.
Я хотел показать, что каждому доступен простой эксперимент, который покажет, когда нужно и когда не нужно избавляться от ветвлений в логике. Мы берём разные реализации и сравниваем между собой хотя бы на сферических тестах в вакууме… и в этом есть смысл, о чём я и философствую дальше.
Методология эксперимента
Разумеется, что каждый, кто серьёзно занимался оптимизацией программ, обратил внимание на сильную искусственность подобных замеров времени. Смотрите: мы берём функцию, создаём для её работы идеальные условия и тестируем её в этих идеальных условиях. В моём случае таким условием был простейший цикл. По времени работы этого цикла с вызовом функции в нём и по времени работы пустого цикла мы пытаемся узнать об эффективности самой функции… и кому-то могло показаться, что я так и делаю. На самом деле всё не так просто, потому что так делать нельзя.
В реальных условиях ваша функция будет окружена другим набором инструкций, цикл, в котором она будет запускаться, может оказаться более длинным и запутанным и время работы этой функции может оказаться абсолютно другим. В каком-то смысле такие бенчмарки, как у меня, напоминают показуху в каком-нибудь магазине, который решил посетить высокий чиновник: цены в нём неожиданно становятся в 10 раз ниже, а зарплата персонала в 10 раз выше… но ровно до тех пор, пока чиновник не уйдёт из магазина. Так зачем же вообще заниматься этим, если результат не отражает реальность? Потому что если правильно понимать словосочетание «отражает реальность», то всё встанет на свои места.
А дело всё в том, что я не делаю выводов о скорости работы функции отдельно. Я делаю вывод о том, которая из функций будет быстрее, а которая медленнее именно в этих тепличных условиях. При этом меня интересует большой отрыв по скорости, потому как небольшой отрыв часто затирается сложностью других участков программы, а вот большой отрыв гарантирует (по крайней мере, в моём опыте работы), что в реальных условиях разница будет такой же… при это НЕ важно, будет ли сама функция работать в 100 раз дольше по сравнению с идеальными условиями – выигрывать у более медленной она будет приблизительно с тем же отрывом, что я получаю в этих идеальных условиях. Честно скажу, я не знаю, так ли это в обычных бытовых задачах, но в научных расчётах, когда нужны миллионы машинных часов счёта, это можно считать аксиомой. В моей практике было только так и никак иначе. Теперь давайте попробуем философски осмыслить вообще ценность любого исследования.
В нашем мире значительная часть экспериментов (даже социальных) не лишена того недостатка, что все они искусственные, но по результатам этих экспериментов люди всё равно могут весьма достоверно предсказать некую ситуацию в реальности. Скажем, возьмите то же соревнование по стайерскому бегу. Несколько людей одеваются в беговые трусы, майку и «тапки» (иногда с шипами), делают специальную разминку, выходят на старт и начинают бежать по идеально гладкой и мягкой дорожке, например, 5 км. Это идеальные условия, в которых мастер спорта проходит дистанцию за 14 минут. Если нацепить на мастера валенки и заставить надеть шубу, то он пробежит ту же дистанцию медленнее, особенно по грязи и лужам, скажем, за 18-20 минут… но это всё равно быстрее, чем обычный неподготовленный человек пробежит даже в идеальных условиях. А в равных условиях у него вообще ноль шансов. Можно взять и другие обычные условия: нужно добежать до уходящего автобуса (или успеть на «зелёный» на пешеходном переходе). Простой вопрос: у кого больше шансов успеть на него при достаточно большой дистанции – у мастера по стайерскому бегу или у обычного человека? Понятно, что у первого, причём шансы практически не меняются в зависимости от формы одежды и многих других условий. Однако данное предположение (причём весьма надёжное) мы делаем только на основе того, что видели, как мастера бегают 5 км за 14 минут. Мы просто делаем предсказание на основе знаний, полученных в идеальных условиях. И с огромной вероятностью эти предсказания будут истинными в условиях реальных.
В мире программирования, конечно, всё гораздо сложнее. Например, с чего это я взял, что мои условия, описанные выше, идеальные? Это я их так назвал, а может оказаться, что в реальной программе компилятор найдет способ так «размазать» мою функцию по коду программы, что время её работы станет равным нулю (она будет вычислена параллельно с какими-нибудь сложными операциями неподалёку). Да, такое может быть, и при жёсткой оптимизации программ опытный программист попробует так изменить алгоритм, чтобы максимально сбалансировать команды для одновременного исполнения инструкций, входящих в них, а процессор потом на ходу перемешает эти инструкции ещё лучше. Но это другой разговор, потому что подобные оптимизации не выполняются отдельно для отдельных простых функций вроде sign или abs, всё делается иначе: мы берём узкий участок кода и смотрим на него целиком, на то, можно ли с ним что-то сделать (даже, может, полностью переклеить), чтобы его логический смысл остался прежним, но сложность уменьшилась. У нас ситуация иная: мы хотим выяснить то, насколько быстрой будет та или иная реализация отдельной небольшой функции, предполагая, что эта отдельная небольшая функция будет достаточно сильно нагружена в некотором процессе, но не настолько, чтобы как-то жёстко её оптимизировать и размазывать её по каким-то другим участкам программы.
Именно так часто и пишутся обычные эффективные программы: когда алгоритмическая эффективность устраивает программиста, он достигает дополнительной практической эффективности тем, что использует хорошо оптимизированные отдельные функции, но глубже не лезет, потому что это может оказаться накладно, и ему проще увеличить вдвое число ядер, чем потратить уйму времени, чтобы добиться такого же ускорения на прежнем их количестве. Вот эти хорошо оптимизированные функции пишутся… внимание… под идеальные условия! Та же библиотека MPIR для длинной арифметики, посмотрите код и увидите там множество реализаций функций низкого уровня, заточенных под разные процессоры, причём MPIR будет выигрывать у вашего самопального кода длинной арифметики как на тестах вроде моих (сферических в вакууме), так и в реальных условиях, когда данные имеют не очень предсказуемый характер (понятно, что победить MPIR можно и очень легко, когда заранее знаешь некоторые серьёзные особенности входящих чисел, но я говорю о том, когда не знаешь). И таких примеров в научном мире полно. Функция factor в Maple будет рвать вашу самопальную функцию факторизации полиномов как в идеальных условиях, когда вы измеряете время работы путём многократного повторения случайных тестов одного за другим, так и в реальных программах, где факторизация занимает ощутимую долю ваших вычислений (например, при какой-нибудь работе с рациональными дробями). Конечно, я допускаю то, что вы можете соревноваться с factor из Maple, но таких людей очень мало, а речь идёт об обычных пользователя, которые хотят написать более-менее хорошую программу, но затрудняются в выборе той или иной реализации сильно нагруженной функции.
Что я хочу сказать: не знаю, как в обычном IT-мире, но в научной вычислительной сфере существует чёткая корреляция между бенчмарками вроде моих (сферических) и реальным поведением тестируемых функций в сложных расчётах, когда эти функции вносят ощутимый вклад в сложность всей программы. То есть ежели некая функция f победила функцию g на сферических тестах в 10 раз, то примерно так же дело будет обстоять в реальной программе. И в этом я неоднократно убеждался с помощью профилировщика.
Поясню ещё один момент: в реальных задачах я не встречал необходимости оптимизировать функции min, max, sign и abs. Обычно они встречаются в группе значительно более сложных вычислений, поэтому совершенно незаметны в таблице с результатами профилировки. Просто я часто встречаю программистов, которые считают своим долгом исковеркать код на основе своих интуитивных предположений об оптимизации, тогда как узкое место их программы вообще в другой точке. Не надо так делать.
Тем не менее, мой эксперимент с этими функциями всё же имел для меня смысл, не смотря на искусственность и оторванность от реальности его кажущейся необходимости. Это я объясняю дальше.
Цели эксперимента
Напомню, что основной целью было получить от сообщества обратную связь в виде замечаний, рекомендаций и вообще некоторого поведения. Анализируя всё это, я делаю выводы и могу теперь сделать аналогичные эксперименты более интересными и полезными. Здесь я лишь тренировался и выражаю благодарность всем, кто принял посильное участие.
Вторая цель – проверить качество измерения времени моим способом. Принимая во внимание тот факт, что у кого-то время получалось отрицательным, можно с уверенностью сказать, что над замерами придётся ещё подумать. Это хорошо, потому что аналогичный провал в серьёзном эксперименте был бы для меня очень тяжёлым испытанием.
Третья цель – проверить способность потенциальных участников на Хабре правильно воспринимать ситуацию и действовать с учётом даже возможных отклонений от плана. Такая способность меня вполне устраивает, хотя меня немного разочаровало то, что некоторые участники предоставляли результаты тестирования, когда программа компилировалась без каких-либо ключей оптимизации… я не знаю, какой смысл в таких измерениях. По крайней мере, в научных расчётах это может быть полезно только для отладки. Тут отчасти моя вина, что я не пояснил данный момент, хотя нужно было догадаться, что раз все программисты очень разные, нужно максимально точно формулировать условия. Подобные пробные эксперименты учат пониманию этого, что тоже полезно.
Четвёртая цель – посмотреть на соотношение времён работы функций на разных процессорах и с разными компиляторами. Вот здесь меня удивил один момент. У некоторых пользователей получалось, что функция minu0 работала в разы медленнее остальных семи функций для минимума и максимума. Вот примеры (это именно при хаотичной подаче данных):
Intel(R) Core(TM) i3-2100 CPU @ 3.10GHz GCC 4.9.3-r3 Опции: -std=gnu++11 -O3 mini: 1.22 vs 2.59 maxi: 1.19 vs 2.71 minu: 13.64 vs 3.01 maxu: 1.21 vs 2.54 Intel Core i7-6700K CPU @ 4.00GHz GCC 5.2.1 Опции: -O3 -std=gnu++11 mini: 0.49 vs 0.83 maxi: 0.48 vs 0.82 minu: 10.20 vs 0.74 maxu: 0.49 vs 0.91 Intel(R) Core(TM)2 Duo CPU E7300 @ 2.66GHz GCC 4.8.4 Опции: g++ -std=gnu++11 -О3 sign: 12.95 vs 2.56 abs: 12.74 vs 0.91 mini: 2.31 vs 3.07 maxi: 2.19 vs 3.19 minu: 15.79 vs 3.54 maxu: 2.08 vs 3.77 Raspberry Pi 3, SoC: Broadcom BCM2837, CPU: ARM Cortex-A53 @ 1.2GHz gcc version 4.9.2 (Raspbian 4.9.2-10) options: -std=gnu++11 -O3 mini: 10.74 vs 17.93 maxi: 10.74 vs 14.33 minu: 24.63 vs 7.16 maxu: 10.74 vs 7.16
И так далее, этих примеров много. Причём здесь везде тупил GCC, потому как clang делал всё правильно:
Intel(R) Core(TM) i5-4210U CPU @ 1.70GHz: Clang 3.7 with Microsoft CodeGen (v140_clang_3_7): Full optimization (-O3) mini: 0.41 vs 2.61 maxi: 0.35 vs 9.28 minu: 0.69 vs 2.96 maxu: 0.44 vs 8.83
Там же в комментариях ещё много примеров работы Clang. Не буду приводить примеры, но VC++ 2015 тоже сделал всё правильно. Таким образом, разработчикам компилятора GCC данные примеры должны быть полезны для отладки блока оптимизации. То есть мой эксперимент выявил косяк компилятора, который может проявиться потом где-то в серьёзной программе.
Можно выделить и некоторые другие результаты, заслуживающие внимания, например, в некоторых случаях страшная формула без ветвлений обнаруживает минимум или максимум быстрее формулы с ветвлением. Вот фрагмент:
Raspberry Pi 3, SoC: Broadcom BCM2837, CPU: ARM Cortex-A53 @ 1.2GHz gcc version 4.9.2 (Raspbian 4.9.2-10) options: -std=gnu++11 -O3 minu: 24.63 vs 7.16 maxu: 10.74 vs 7.16
В первой строке ясно – это глюк GCC, о котором я писал выше, а во второй именно поражение метода с ветвлением.
Пятая цель была косвенно обозначена выше – показать обычным программистам, что не нужно фанатично придерживаться какой-то догмы. В каждом случае свой вариант может оказаться лучше и нужно проводить тестирование, чтобы сделать выбор, а не полагаться на слепую уверенность, основанную на оптимизации под четвёртый пень, возникшую в те далёкие времена, когда они учились в университете.
Шестая цель, менее всего значимая, – мне нужно было поделиться своими материалами (ссылки [4-7] из предыдущей статьи), чтобы со временем получить обратную связь, понять, верно ли я двигаюсь, когда пишу подобные статьи наметить для себя дальнейший план работы и отыскать единомышленников. Эту обратную связь я пока не получил, но всему своё время.
Седьмая цель, косвенная, — получить повод написать этот пост, делясь в нём своими мыслями, и провести в нём голосование.
Предлагаю голосовать. Если 80% участников голосования посчитают, что имеет смысл продолжать похожие эксперименты на Хабре (разумеется, более аккуратно), я продолжу и шаг за шагом разберу много разных алгоритмов. Польза для сообщества – обучение, ведь в процессе экспериментов я всегда объясняю или показываю, где прочитать объяснение тех вещей, что тестируются. Другая польза – возможность проверить всё на своём компьютере. Польза для меня – критика, корректировка, подсказки и советы. Если 80% не наберётся, я соберу свою аудиторию для таких экспериментов сам, просто это займёт больше времени, но зато я не буду здесь мешаться.
Голосуем и до новых встреч: )
PS. Пояснение к голосованию: под «более серьёзным» экспериментом подразумевается не более сложный алгоритм (алгоритмы будут разными — от sign(x) до Шёнхаге — Штрассена), а я буду стараться как-то улучшать их качество и потенциальную ценность результата.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (11)
DrSmile
17.04.2016 20:51+1Есть один достаточно важный случай, когда отношение производительности двух вариантов реализации серьезно меняется при переходе от идеальных условий к реальным. Пусть у нас есть расчет некоторой функции с помощью полиномиального приближения и с помощью таблицы. Тогда в идеальных условиях таблица целиком попадет в L1 и может обогнать расчет полинома, однако в реальных условиях она будет вытеснена из кеша другими данными и скорость доступа к ней серьезно упадет, тогда как полиномиальное приближение будет работать также быстро.
Zealint
17.04.2016 21:36+1Это называется неправильная постановка эксперимента. Если я знаю, что в реальных условиях на работу функции влияют её условия взаимодействия с кэшем, то буду моделировать эксперимент с учётом этих условий. В эксперименте будут идеальные условия, но с тем же отношением к кэшу. Всё это нужно хорошо заранее предусмотреть, и никто не мешает это сделать, когда знаешь условия задачи заранее. В практике эффективного программирования вообще редко бывает, чтобы задача решалась для универсального случая, чаще всего решают конкретную задачу в конкретных условиях, причём нередко даже учитывают специфику конкретного компьютера. И всё это тоже нетрудно смоделировать.
Shamov
18.04.2016 17:06Но если вы не пытались делать выводов о скорости работы функции отдельно, то зачем было измерять время работы цикла без функции и вычитать его из времени выполнения цикла с функцией? В чём смысл этой операции? Чтобы понять какая из функций работает быстрее именно в этих конкретных условиях, не нужно специально пытаться нейтрализовать влияние этих условий на результат. Наоборот, нужно сравнивать между собой интегральные замеры, включающие время выполнения как самой функции, так и окружающих её условий.
Zealint
18.04.2016 17:37Причин здесь несколько, не все из которых на данный момент могут быть оглашены (за недостатком фактических сведений). Напишу две. Обозначим чистое время работы цикла через C, а точное (если бы мы его знали) время работы тестируемой функции — через A. Совершенно очевидно, что одновременная работа цикла и тестируемой функции будет более быстрой, чем А+С, это именно то, что Вы зачем-то повторяли мне в комментариях к прошлой статье и про что я сразу сказал, что это понятно, а Вы упорно думали, будто я этого не знаю. Инструкции могут исполняться одновременно с определённой степенью этой одновременности. То есть их время работы пересекается в некоторой степени. Для начала мне нужно было определить, будет ли степень пересечения A и C сильно отличаться на разных машинах. Для этого, конечно, можно было поступить иначе, но я решил просто их вычитать и ожидать отрицательного или близкого к нулю значения. Оно появилось. С другой стороны, отрицательное значение может означать серьёзные ошибки самой методики измерения (как мне сообщили, на AMD подобная «замерялка» вообще может глючить в таком виде). Далее, вторая причина, по которой я вычитал — на практике, когда тестируются более сложные функции и значительно более сложным набором входных данных (генерация которых может быть тяжёлой), данная корректировка успешно работает. Она даёт действительно чистое время работы функции («чистое» — это просто название), которое остаётся таким же в реальных условиях. В случае маленьких функции такая корректировка оказывается слишком грубой. Чтобы оценить степень этой грубости, мне нужно всё проверить в разных условиях, и я проверил. Если бы мне нужно было именно чистое время таких маленьких функций, я запускал бы их в разных циклах с разными окружающими помехами, а затем получал бы систему уравнений с несколькими неизвестными, из которых надёжно рассчитывал бы оценку на чистое время. Получив нужную мне информацию, я теперь должен сообразить, как скорректировать подобные эксперименты на будущее. Предвидя Ваш ответ, сразу говорю: ДА, можно было сделать по-другому и, ДА, можно было сделать умнее или глупее, но это будет уже Ваш эксперимент, а не мой. Я же своих целей на данный момент почти добился с минимальными затратами. Для Вас же повторю простую формулу: не нравится — критикуй, критикуя — предлагай, предлагаешь — делай, а делаешь — отвечай.
Shamov
18.04.2016 17:52Вот если бы вы сразу сказали, что отрицательное время в рамках вашего эксперимента имеет смысл, можно было бы сэкономить кучу сил. Тогда я сразу бы понял, что вообще не понимаю вашего замысла. К слову, я и сейчас его не понимаю. Но теперь эксперимент выглядит так, как будто в нём есть какой-то смысл… пусть и непонятный мне.
anton0xf
19.04.2016 12:21+2// Прошу прощения, что комментирую эту статью, а не предыдущую, но туда мне писать уже нельзя.
1. Вы можете объяснить, почему в ваших собственных результатах тестирования в предыдущей статье (как и у большинства отписавшихся), большинство функций отработали заметно быстрее при «хаотичной подаче»?
2. Как я понимаю, ваш способ получения «хаотической» последовательности чисел всё ещё оставляет огромное пространство для различных оптимизаций при компиляции и исполнении. Почему вы не используете случайные числа (как предложил meduzik: habrahabr.ru/post/281629/#comment_8854253)?
3. Есть какой-то факт, гарантирующий, что первые 2^32 элементов последовательность вроде «a[i+1] = (19993 * a[i] + 1) % 2^32» переберут все натуральные числа от 0 до 2^32 — 1 по одному разу?Zealint
19.04.2016 12:35+1Хорошие вопросы, отвечаю.
Первый вопрос у меня пока в очереди на обработку, сейчас я не знаю на него ответа.
Второй вопрос: случайные числа использовать не получится, потому что я не знаю ни одного генератора случайных чисел. Псевдослучайные же числа можно генерировать по-разному и мой вариант — это как раз и есть так называемый линейный конгруэнтный генератор псевдослучайных чисел. Он даёт не очень хорошую последовательноть с точки зрения случайности, но она ганрантированно весьма хаотичная и переберёт все числа из диапазона при наличии трёх условий (ответ на третий Ваш вопрос), перечисленных по указанной ссылке. Например, если взять множитель 11, то данный генератор НЕ перебирает все числа — этот факт, о котором многие не знают, можно довольно забавно использовать чтобы запутать человека, с которым споришь: ) Он начнёт искать ошибку в другом месте и обязательно скажет глупость.
Что касается оптимизации метода получения последовательности, то у меня нет целей его оптимизировать. Но, отвечаю, да, это можно сделать массой способов, вплоть до выбора другого генератора.
Zealint
20.04.2016 09:40+1Отвечаю на Ваш первый вопрос. На самом деле он следует из комментария grechnik, который написан здесь. В случае последовательной версии цикл полностью сворачивается в константу, поэтому время работы пустого цикла равно нулю (ну, или эпсилон), значит я вычитаю ноль на самом деле. В случае хаотичной версии пустой цикл в константу не сворачивается (хотя сумма чисел та же), поэтому там я вычитаю не ноль. Значит получается, что после вычитания в хаотичной версии числа будут заметно меньше, чем в последовательной, где ничего не вычитается. А это значит, что нужно более аккуратно следить за ходом эксперимента. Это одна из тех мыслей, которую выразил halyavin в первом комментарии к этой статье — нужно смотреть, что с вашим кодом сделал компилятор. Мой эксперимент это тоже показывает, и это как раз очень удачно укладывается в его цели (эксперимент же пробный). Спасибо за вопрос.
halyavin
Тестировать микробенчмарки, не глядя на дизассемблер — мракобесие. В видео на CppCon 2015 все это подробно объясняется.
Zealint
Разумеется, никто не спорит. Но мракобесием на мой взгляд также является любое категоричное высказывание, жёстко разграничивающее мракобесие от науки путём озвучивания каких-то универсальных правил работы. Важным является не то, смотрит кто-то в ассемблер или нет, а то, что он в конечном итоге получает и как это соотносится с его целями.