Спасибо за внимательность! Действительно, граничный случай размеров кэша я не учёл, когда писал, что это всё универсальные оптимизации. Тем не менее, при запуске расчётов имеет смысл подбирать размер задачи так, чтобы она полностью помещалась в кэш, да и желательно L2. Например, это как раз верно при размере сетки 100x100 и результат применения предвычислений положительный как на Эльбрусе, так и на Intel (хоть и небольшой, 5%). Вероятно, именно это объясняет, почему при исходных замерах на Intel было замедление.
Вопрос ассоциативности — это уже более глубокое погружение и необходимость задумываться о свойствах конкретного процессора. Предлагаемый пример опять же можно отнести на этап подбора размера задачи. Например, кажется, что нет ничего плохого в том, чтобы увеличить количество точек до степени 2, если это позволит считать быстрее. Попробую на досуге проверить на этой задаче, хоть тут и может быть сложно подобрать подходящее разложение MxN (иначе вылезут и другие эффекты).
Да, конечно, именно этому посвящён предпоследний раздел: демонстрируется, что то же количество арифметики при отказе от одной загрузки из памяти работает быстрее. Я показал это на примере Эльбруса, такое же поведение на Intel. Только IBM выбился: при наличии бесполезной арифметической операции и отказе от загрузки из памяти он ускорился (время было около 4 с), но ещё большее ускорение получилось после того, как убрал лишнюю арифметику (итоговые 3.8 с, вошедшие в таблицу).
Тем не менее, явно нельзя отрицать, что от векторизации и прочих шагов есть положительный эффект. В общем-то, именно они привели к состоянию, когда на первый план вышла память. С удовольствием продемонстировал бы что-нибудь ещё на тему памяти, но в этой теме у меня пока нет достаточного опыта, чтобы поделиться им.
Т.е. если хочется померять ускорение непосредственно от инлайна
Возможна, у меня не самая удачная формулировка получилась, но цели измерять ускорение непосредственно от подстановки функции не стояло. Когда хочется ускорить вычисления, не совсем важно, как именно получено ускорение, намного важнее, что нужно сделать, чтобы его получить. Идея здесь в том, что возможность подстановки функции открывает дорогу другим важным оптимизациям, которые невозможны просто за счёт предсказателя переходов. Ну и, конечно, я не пытаюсь теоретизировать на общих основаниях, это пока не мой уровень, здесь речь только о классе вычислительных задач, в том или ином виде подобных этой.
может сложиться впечатление, что векторизация на том же Интеле вообще ничего не даёт
Да, нехорошо получилось. Приписал, что это "ручная векторизация", теперь недопонимания вроде не должно возникнуть.
Также было бы здорово привести хотя бы конечные версии ассемблерного кода для x86/IBM.
Не хотелось загромождать статью, но вот только сейчас пришла идея, что будет вполне удобным для всех решением оформить это дело в виде файлов в репозитории. Так что чуть позже добавлю, а сейчас приведу основной цикл на AMD64 (его я проверял глазами, а вот аналога -fverbose-asm на IBM я не нашёл и не знаком с его ассемблером, так что не могу ничего сказать). FMA, естественно, есть, иначе бы пришлось что-то с этим делать, потому что это было бы совсем не честно.
Спасибо за совет, попробовал на двух вариантах (2 и 8 в таблицах), но ускорения это не дало. Если смотреть пример из документации, эта опция должна быть полезна при смешении 32-битных и 64-битных типов и когда-то ещё. Что ж, здесь не повезло...
Детали сейчас не помню, но без "фокуса с разложением в ряд" не получается второй порядок аппроксимации. Я, естественно, это всё сам не изобретал, а делал по описанию в задании, которое полностью соответствовало рассказанному на курсе по численным методам несколько лет назад. В одной из книг видел простое описание, сейчас быстро не нашёл, так что пока могу только предложить посмотреть страницу 190 из приведенной в статье книги, вроде это оно.
Вышла небольшая заминка по времени, но теперь наконец добавил пример для правой границы под спойлер. Для остальных границ получается аналогично, в коде у меня по сути отражена та же самая формула, просто написанная не в TeX.
Да, кстати, пока писал, задумался о том, что "просто так" выразить задачу в матричном виде не получится: из-за шаблона "крест" придётся рассматривать неизвестный вектор размера MN, а матрица будет иметь размер MNxMN. Интересно, как справится общий метод при таких размерах.
Вынужден обвинить вас в тотальной невнимательности. Полные исходники размещены на github, об этом сказано в статье, дана ссылка, потом внизу ссылка продублирована. А дальше ещё хуже: я пишу, что замеряется время выполнения 1000 шагов, а вы мне сейчас приводите непонятный расчёт скорости памяти. Мне кажется, лучше сначала читать статью, а потом идти в комментарии (да-да, только в комментариях упоминается 428000, это вас выдаёт).
Могу предположить, что минусуют вас за форму подачи и бессодержательность первого комментария. Написали бы сразу развёрнуто, был бы разговор, а то я вот просто проигнорировал первый комментарий, так как уже давал развёрнутый ответ на похожий. Теперь же мне есть, чем дополнить.
Всего в алгоритме участвует 5 основных матриц, так что кажется справедливым смотреть на то, чтобы они все поместились в кэш одного уровня. При размере 1000x1000 это 38 МБ, не помещается в L3 ни на Intel, ни на Эльбрусе.
Ну как же не упомянуто? Я уже и в комментарии Вам процитировал, вот ещё раз приведу выдержку из текста статьи: "компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов".
Давайте сойдёмся на том, что я не претендовал на "достаточно высокий в целом" уровень материала, и не будем говорить о недостаточности объёма трудозатрат. Объём определялся тем, что мне интересно, о чём я хотел поделиться, и соответствием теме статьи.
Вроде из названия и текста чётко видно, что речь идёт о подходах к оптимизации на Эльбрусе. Что результаты на двух других архитиктурах приведены для "контроля адекватности" и демонстрации универсальности применяемых подходов, а не являются бенчмарком или сравнением процессоров/компиляторов. Хорошо, обещаю в следующий раз постараться не вводить Вас в заблуждение.
Я рассчитывал, что мне можно поверить на слово :) Собственно, никакого секрета здесь нет: наличие векторизации на ymm регистры я проверил по ассемблерному коду (есть вхождение тела цикла, соответствующее обработке по 4 элемента в ymm регистре), на IBM есть замечательная опция -qlistfmt, по ней создаётся подробный отчёт, в котором видно, что цикл был успешно векторизован и раскручен (на 2, если я правильно помню).
Классический компилятор — это известный инструмент, с которым было понятно, как работать (в частности, касательно использования MPI), он продолжает поддерживаться, обновляться и поставляться в составе набора для HPC применений.
Если честно, Вы хотите слишком многого от меня. Ещё раз повторюсь, было бы интересно всё происследовать и проверить, но статья не о сравнении компиляторов, а о методах оптимизации с прицелом на Эльбрус. Если бы я начал сравнивать с другими компиляторами, мне бы пришлось то же самое провернуть на POWER (как минимум, сравнить с gcc — он тоже есть на машине, к которой у меня доступ) и на Эльбрусе. Ну и во что превратилась бы статья?.. Выбор же icc как дающего лучшие результаты обоснован личным опытом и рядом других факторов. Ни в коем разе не претендую на абсолютную справедливость сделанного выбора.
Очень печально, что вы даже не попытались разобраться ни с решаемой задачей, ни с идеей статьи. Уверенно это заявляю, так как в противном случае вы бы начали с вопросов ("А как Ваши подходы применимы к другим размерам задачи?"), ну и тон был бы менее агрессивным.
Итак, во-первых, все представленные оптимизации являются универсальными для размеров сетки, скажем, от 100x100. На сильно меньших размерах вряд ли понадобится запускать: при запуске в один процесс количество точек решения будет недостаточным, при запуске на много процессов возрастут накладные расходы на обмен данными. Где-нибудь сказано, что существенно используется размер сетки? В определённых местах можно было потребовать чётный размер, например, но этого не сделано, сохранён общий алгоритм. Вообще, все представленные оптимизации направлены на ускорение самой вычислительно объёмной части, которая в рамках одного шага метода составляет O(MN) для сетки размера MxN.
Во-вторых, какое обоснование вас бы удовлетворило? Сделать запуски на всех размерах от 1001x1001 до 1500x1500? Это немного не научно.
В-третьих, о каких таких матрицах особого вида вы здесь говорите? Я честно не понимаю. У меня разностная схема для уравнения Пуассона в двумерной области и метод наименьших невязок. А что вы придумали в качестве "решаемой в статье" задачи — решение обычной СЛАУ?
В-четвёртых, да, если в какой-либо (любой, не этой) задаче появляются матрицы особого вида, то к ним применяют другие подходы. Начиная с асимптотически менее сложных алгоритмов, использующих структуру матриц, и заканчивая отдельными реализациями, эффективно работающими с представлением в памяти.
Они появляются после выполнения преобразований над аппроксимацией граничных условий. Формула корректна, я проверял её. Сейчас не могу подробно написать, но вечером готов выписать формулы и обоснование.
Так ведь всё написано: "Заметим, что на рассматриваемых процессорах Intel и IBM нет требований к выравниванию данных, а потому компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов". Думаю, из флагов оптимизации под текущий процессор (-xHost, -O5) понятно, что использовались AVX2 и 128-битные регистры на Power (не помню, как точно они сейчас называются, поэтому не пишу AltiVec).
Много чего было бы неплохо сделать, но статья посвящена конкретно рассмотрению Эльбруса и ориентирована на него. Уверен, материалов по Intel и IBM можно найти намного больше и более полных.
Это не бенчмарк, прошу материал в таком качестве не рассматривать. Что же касается запуска и ожидаемых результатов, эта информация будет оформлена в файле readme, который появится позднее.
Спасибо, полностью согласен, это недосмотр с моей стороны! Добавил файл с лицензией.
Вы как-то одним махом отбросили замечательный (без сарказма, речь о многопоточной работе и SMT8) суперскалярный IBM, который изначально был всего в 5 раз быстрее. Он ускорился в 13 раз, это сложно назвать "экономит". Да даже разница в 4 раза в случае Intel может вылиться Вам в неприятный сюрприз по стоимости времени работы на вычислительном кластере.
Кстати, предлагаю посчитать, сколько рабочего времени квалифицированных разработчиков нужно, чтобы исправить поделия малоквалифицированных, когда это становится нужно.
А, понял. Я прошу прощения, это было недопонимание с моей стороны, успел забыть некоторые вещи. По сути, у меня и есть реализация метода наименьших невязок (который MINRES как частный случай GMRES, только здесь с более мягким условием останова).
В рамках учебной задачи, естественно, нужно было реализовывать самому. Спасибо за наводку, интересно будет сравнить, как соотносится ручной вариант с использованием оптимизированных библиотек.
В статье это сказано: рассматривается разностная схема с сеткой размера 1000x1000, то есть по 1000 точек на каждом направлении. Количество шагов итерационного процесса для этой сетки до выполнения условия останова составляет примерно 428000. О применимости других методов я, честно говоря, не слышал и быстро найти не смог. Если Вас не затруднит, был бы рад ссылкам на литературу с обоснованием возможности получить приближённое решение требуемой точности.
Мне неизвестно о существовании нужных функций в библиотеках типа MKL. Подчеркну, что сложность одной итерации в этом методе O(MN), то есть здесь нет умножения матриц или "обычного" решения СЛАУ.
Касательно библиотек для Эльбруса: есть оптимизированная EML, отвественность за её разработку лежит на МЦСТ, соответственно, там применены более продвинутые техники, можете почитать в Руководстве.
Без проблем. Функцию (не)сортировки я уже привёл ссылкой на Compiler Explorer (проверив, что внутренний цикл остался без изменений после того, как исчез внешний). Соответственно, при сравнении с x86 придётся тоже проверить, что внутренний цикл сохраняется в неизменном виде.
Спасибо, что обратили внимание! Действительно, не пригляделся достаточно внимательно, а там появились ещё и дополнительные инструкции для замера, замыливающие глаз. Вот и доверяй людям... Тогда беру назад все слова из предыдущего комментария.
Ну согласитесь, ситуация, когда для получения самого лучшего результата надо вместо -O4 поставить -O2, это достаточно странно. Понятно, что об этом догадаться невозможно.
Странная? Странная! Кто бы мог подумать, что то же самое происходит на x86. Встаёт вопрос, почему Вы в одном случае выбираете -O2, а в другом -O4, хотя для lcc заявляется примерно аналогичное gcc поведение для уровня -O2 и включение агрессивных платформо-зависимых оптимизаций с -O3. Честно говоря, я всё больше начинаю думать, что Вы проверили все эти комбинации, а в публикацию пустили наиболее выгодный для Вас вариант.
Спасибо за внимательность! Действительно, граничный случай размеров кэша я не учёл, когда писал, что это всё универсальные оптимизации. Тем не менее, при запуске расчётов имеет смысл подбирать размер задачи так, чтобы она полностью помещалась в кэш, да и желательно L2. Например, это как раз верно при размере сетки 100x100 и результат применения предвычислений положительный как на Эльбрусе, так и на Intel (хоть и небольшой, 5%). Вероятно, именно это объясняет, почему при исходных замерах на Intel было замедление.
Вопрос ассоциативности — это уже более глубокое погружение и необходимость задумываться о свойствах конкретного процессора. Предлагаемый пример опять же можно отнести на этап подбора размера задачи. Например, кажется, что нет ничего плохого в том, чтобы увеличить количество точек до степени 2, если это позволит считать быстрее. Попробую на досуге проверить на этой задаче, хоть тут и может быть сложно подобрать подходящее разложение MxN (иначе вылезут и другие эффекты).
Да, конечно, именно этому посвящён предпоследний раздел: демонстрируется, что то же количество арифметики при отказе от одной загрузки из памяти работает быстрее. Я показал это на примере Эльбруса, такое же поведение на Intel. Только IBM выбился: при наличии бесполезной арифметической операции и отказе от загрузки из памяти он ускорился (время было около 4 с), но ещё большее ускорение получилось после того, как убрал лишнюю арифметику (итоговые 3.8 с, вошедшие в таблицу).
Тем не менее, явно нельзя отрицать, что от векторизации и прочих шагов есть положительный эффект. В общем-то, именно они привели к состоянию, когда на первый план вышла память. С удовольствием продемонстировал бы что-нибудь ещё на тему памяти, но в этой теме у меня пока нет достаточного опыта, чтобы поделиться им.
Возможна, у меня не самая удачная формулировка получилась, но цели измерять ускорение непосредственно от подстановки функции не стояло. Когда хочется ускорить вычисления, не совсем важно, как именно получено ускорение, намного важнее, что нужно сделать, чтобы его получить. Идея здесь в том, что возможность подстановки функции открывает дорогу другим важным оптимизациям, которые невозможны просто за счёт предсказателя переходов. Ну и, конечно, я не пытаюсь теоретизировать на общих основаниях, это пока не мой уровень, здесь речь только о классе вычислительных задач, в том или ином виде подобных этой.
Да, нехорошо получилось. Приписал, что это "ручная векторизация", теперь недопонимания вроде не должно возникнуть.
Не хотелось загромождать статью, но вот только сейчас пришла идея, что будет вполне удобным для всех решением оформить это дело в виде файлов в репозитории. Так что чуть позже добавлю, а сейчас приведу основной цикл на AMD64 (его я проверял глазами, а вот аналога -fverbose-asm на IBM я не нашёл и не знаком с его ассемблером, так что не могу ничего сказать). FMA, естественно, есть, иначе бы пришлось что-то с этим делать, потому что это было бы совсем не честно.
Векторизованный [alpha != 0] цикл из calc_aw_b:
Спасибо за совет, попробовал на двух вариантах (2 и 8 в таблицах), но ускорения это не дало. Если смотреть пример из документации, эта опция должна быть полезна при смешении 32-битных и 64-битных типов и когда-то ещё. Что ж, здесь не повезло...
Детали сейчас не помню, но без "фокуса с разложением в ряд" не получается второй порядок аппроксимации. Я, естественно, это всё сам не изобретал, а делал по описанию в задании, которое полностью соответствовало рассказанному на курсе по численным методам несколько лет назад. В одной из книг видел простое описание, сейчас быстро не нашёл, так что пока могу только предложить посмотреть страницу 190 из приведенной в статье книги, вроде это оно.
Вышла небольшая заминка по времени, но теперь наконец добавил пример для правой границы под спойлер. Для остальных границ получается аналогично, в коде у меня по сути отражена та же самая формула, просто написанная не в TeX.
Да, кстати, пока писал, задумался о том, что "просто так" выразить задачу в матричном виде не получится: из-за шаблона "крест" придётся рассматривать неизвестный вектор размера MN, а матрица будет иметь размер MNxMN. Интересно, как справится общий метод при таких размерах.
Вынужден обвинить вас в тотальной невнимательности. Полные исходники размещены на github, об этом сказано в статье, дана ссылка, потом внизу ссылка продублирована. А дальше ещё хуже: я пишу, что замеряется время выполнения 1000 шагов, а вы мне сейчас приводите непонятный расчёт скорости памяти. Мне кажется, лучше сначала читать статью, а потом идти в комментарии (да-да, только в комментариях упоминается 428000, это вас выдаёт).
Могу предположить, что минусуют вас за форму подачи и бессодержательность первого комментария. Написали бы сразу развёрнуто, был бы разговор, а то я вот просто проигнорировал первый комментарий, так как уже давал развёрнутый ответ на похожий. Теперь же мне есть, чем дополнить.
Всего в алгоритме участвует 5 основных матриц, так что кажется справедливым смотреть на то, чтобы они все поместились в кэш одного уровня. При размере 1000x1000 это 38 МБ, не помещается в L3 ни на Intel, ни на Эльбрусе.
Ну как же не упомянуто? Я уже и в комментарии Вам процитировал, вот ещё раз приведу выдержку из текста статьи: "компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов".
Давайте сойдёмся на том, что я не претендовал на "достаточно высокий в целом" уровень материала, и не будем говорить о недостаточности объёма трудозатрат. Объём определялся тем, что мне интересно, о чём я хотел поделиться, и соответствием теме статьи.
Вроде из названия и текста чётко видно, что речь идёт о подходах к оптимизации на Эльбрусе. Что результаты на двух других архитиктурах приведены для "контроля адекватности" и демонстрации универсальности применяемых подходов, а не являются бенчмарком или сравнением процессоров/компиляторов. Хорошо, обещаю в следующий раз постараться не вводить Вас в заблуждение.
Я рассчитывал, что мне можно поверить на слово :) Собственно, никакого секрета здесь нет: наличие векторизации на ymm регистры я проверил по ассемблерному коду (есть вхождение тела цикла, соответствующее обработке по 4 элемента в ymm регистре), на IBM есть замечательная опция -qlistfmt, по ней создаётся подробный отчёт, в котором видно, что цикл был успешно векторизован и раскручен (на 2, если я правильно помню).
Классический компилятор — это известный инструмент, с которым было понятно, как работать (в частности, касательно использования MPI), он продолжает поддерживаться, обновляться и поставляться в составе набора для HPC применений.
Если честно, Вы хотите слишком многого от меня. Ещё раз повторюсь, было бы интересно всё происследовать и проверить, но статья не о сравнении компиляторов, а о методах оптимизации с прицелом на Эльбрус. Если бы я начал сравнивать с другими компиляторами, мне бы пришлось то же самое провернуть на POWER (как минимум, сравнить с gcc — он тоже есть на машине, к которой у меня доступ) и на Эльбрусе. Ну и во что превратилась бы статья?.. Выбор же icc как дающего лучшие результаты обоснован личным опытом и рядом других факторов. Ни в коем разе не претендую на абсолютную справедливость сделанного выбора.
Очень печально, что вы даже не попытались разобраться ни с решаемой задачей, ни с идеей статьи. Уверенно это заявляю, так как в противном случае вы бы начали с вопросов ("А как Ваши подходы применимы к другим размерам задачи?"), ну и тон был бы менее агрессивным.
Итак, во-первых, все представленные оптимизации являются универсальными для размеров сетки, скажем, от 100x100. На сильно меньших размерах вряд ли понадобится запускать: при запуске в один процесс количество точек решения будет недостаточным, при запуске на много процессов возрастут накладные расходы на обмен данными. Где-нибудь сказано, что существенно используется размер сетки? В определённых местах можно было потребовать чётный размер, например, но этого не сделано, сохранён общий алгоритм. Вообще, все представленные оптимизации направлены на ускорение самой вычислительно объёмной части, которая в рамках одного шага метода составляет O(MN) для сетки размера MxN.
Во-вторых, какое обоснование вас бы удовлетворило? Сделать запуски на всех размерах от 1001x1001 до 1500x1500? Это немного не научно.
В-третьих, о каких таких матрицах особого вида вы здесь говорите? Я честно не понимаю. У меня разностная схема для уравнения Пуассона в двумерной области и метод наименьших невязок. А что вы придумали в качестве "решаемой в статье" задачи — решение обычной СЛАУ?
В-четвёртых, да, если в какой-либо (любой, не этой) задаче появляются матрицы особого вида, то к ним применяют другие подходы. Начиная с асимптотически менее сложных алгоритмов, использующих структуру матриц, и заканчивая отдельными реализациями, эффективно работающими с представлением в памяти.
Они появляются после выполнения преобразований над аппроксимацией граничных условий. Формула корректна, я проверял её. Сейчас не могу подробно написать, но вечером готов выписать формулы и обоснование.
Так ведь всё написано: "Заметим, что на рассматриваемых процессорах Intel и IBM нет требований к выравниванию данных, а потому компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов". Думаю, из флагов оптимизации под текущий процессор (-xHost, -O5) понятно, что использовались AVX2 и 128-битные регистры на Power (не помню, как точно они сейчас называются, поэтому не пишу AltiVec).
Много чего было бы неплохо сделать, но статья посвящена конкретно рассмотрению Эльбруса и ориентирована на него. Уверен, материалов по Intel и IBM можно найти намного больше и более полных.
Это не бенчмарк, прошу материал в таком качестве не рассматривать. Что же касается запуска и ожидаемых результатов, эта информация будет оформлена в файле readme, который появится позднее.
Спасибо, полностью согласен, это недосмотр с моей стороны! Добавил файл с лицензией.
Вы как-то одним махом отбросили замечательный (без сарказма, речь о многопоточной работе и SMT8) суперскалярный IBM, который изначально был всего в 5 раз быстрее. Он ускорился в 13 раз, это сложно назвать "экономит". Да даже разница в 4 раза в случае Intel может вылиться Вам в неприятный сюрприз по стоимости времени работы на вычислительном кластере.
Кстати, предлагаю посчитать, сколько рабочего времени квалифицированных разработчиков нужно, чтобы исправить поделия малоквалифицированных, когда это становится нужно.
А, понял. Я прошу прощения, это было недопонимание с моей стороны, успел забыть некоторые вещи. По сути, у меня и есть реализация метода наименьших невязок (который MINRES как частный случай GMRES, только здесь с более мягким условием останова).
В рамках учебной задачи, естественно, нужно было реализовывать самому. Спасибо за наводку, интересно будет сравнить, как соотносится ручной вариант с использованием оптимизированных библиотек.
В статье это сказано: рассматривается разностная схема с сеткой размера 1000x1000, то есть по 1000 точек на каждом направлении. Количество шагов итерационного процесса для этой сетки до выполнения условия останова составляет примерно 428000. О применимости других методов я, честно говоря, не слышал и быстро найти не смог. Если Вас не затруднит, был бы рад ссылкам на литературу с обоснованием возможности получить приближённое решение требуемой точности.
Мне неизвестно о существовании нужных функций в библиотеках типа MKL. Подчеркну, что сложность одной итерации в этом методе O(MN), то есть здесь нет умножения матриц или "обычного" решения СЛАУ.
Касательно библиотек для Эльбруса: есть оптимизированная EML, отвественность за её разработку лежит на МЦСТ, соответственно, там применены более продвинутые техники, можете почитать в Руководстве.
8 стадий конвейера - очередной миф, почему-то активно всеми распространяемый. Для целочисленной операции 13 стадий и вряд ли найдётся операция с меньшим числом. Вот официальный источник: http://mcst.ru/files/60f6d2/1adece/616a5e/eb0728/tvgi.431281.028re.pdf
Без проблем. Функцию (не)сортировки я уже привёл ссылкой на Compiler Explorer (проверив, что внутренний цикл остался без изменений после того, как исчез внешний). Соответственно, при сравнении с x86 придётся тоже проверить, что внутренний цикл сохраняется в неизменном виде.
А вот так замерял:
Спасибо, что обратили внимание! Действительно, не пригляделся достаточно внимательно, а там появились ещё и дополнительные инструкции для замера, замыливающие глаз. Вот и доверяй людям... Тогда беру назад все слова из предыдущего комментария.
Странная? Странная! Кто бы мог подумать, что то же самое происходит на x86. Встаёт вопрос, почему Вы в одном случае выбираете -O2, а в другом -O4, хотя для lcc заявляется примерно аналогичное gcc поведение для уровня -O2 и включение агрессивных платформо-зависимых оптимизаций с -O3. Честно говоря, я всё больше начинаю думать, что Вы проверили все эти комбинации, а в публикацию пустили наиболее выгодный для Вас вариант.