Намедни работая над одной ошибкой в одном опенсорсном проекте, увидел как коллега (тоже работающий параллельно над той же проблемой) залил такой вот коммит [31a078bec7]:

   	/*
-	 * Select the list item based on the index. Negative operand means
-	 * end-based indexing (-2, ...), and -1 means out of range.
+	 * Decode end-offset index values.
   	 */
-	if (opnd < -1) {
-	    index = opnd+1 + objc;
-	} else {
-	    index = opnd;
-	}
+	index = opnd + (opnd <= TCL_INDEX_END)*(objc - 1 - TCL_INDEX_END);
   	pcAdjustment = 5;

Изменение само по себе правильное (теперь TCL_INDEX_END есть константное определение (-2)).

И грубо говоря в уме это разворачивается в следующее (все переменные int):

index = opnd + cmp(opnd, (-2))==>(0 | 1) * (objc - 1 - (-2));

Т. е. он как бы тем самым хотел сэкономить здесь один условный переход.
И всё как бы ничего, однако меня всё же насторожила такая казалось бы пустячная «оптимизация» с уклоном в арифметику.

Во первых, это изменение касается самой «главной» функции в этом проекте (TEBCresume), ибо она ответственна за исполнение байт-кода (JIT скомпилированных инструкций языка TCL). По этой причине эта функция еще и самая большая (порядка 6 тысяч строк + примитивы и макросы) и одна из самых сложных в кодовой базе проекта, с множественными `goto`, головоломными макросами для работы со «стеком» исполнения, свёртка/развертка NRE (nonrecursive evaluation) и т.д. и т.п.

Т.е. изменения этой функции нередко рассматриваются под лупой, а то и под микроскопом (т.к. бывало что даже незначительные модификации могут перевернуть весь код этой функции с ног на голову)…

Во вторых, по роду деятельности мне часто приходится оптимизировать сишный код, разглядывая его ассемблерное отражение, выжимая доли микро- а то и нано-секунд, и я часто вижу, что там очень всё совсем неоднозначно бывает. Как минимум иногда разворачивая такие вот «экономящие» условный jump конструкции обратно в if или даже if/else, я видел улучшение как и в результирующем ассемблерном коде, так и явно при конечном сравнении производительности результатов исполнения.

Собственно к чему я все это писал — хотелось на примере показать как оно бывает, ну и раз уж коснулись этой темы, собрать немного статистики. Посему пара опросов в конце статьи…

Не стоит забывать про оптимизацию в современных компиляторах, которая достигла если не совершенства, то уже очень и очень на уровне.

Другое дело, что то, что там компилятор наоптимизировал, выходит иногда боком при исполнении на современном железе с собственной оптимизацией внутри, типа внеочередное или спекулятивное исполнение, параллелизм на уровне команд (ILP) и прочее.
Но это уже тема совсем другой статьи.

По понятным причинам, не будем разбирать это всё в оригинальной функции…

Поэтому собсвенно в качестве примера две функции test1 (оптимизация в арифметике), и test2 (flow как оно есть с одним `if`), и результирующий ассемблер для обоих функций в разных версиях компилятора, на примере gcc, проверялось с -O2 и -Ofast (практически без, а в trunk совсем без изменений):
c/c++
int test1(int opnd, int objc) {
  int index;

  index = opnd + (opnd <= (-2)) * 
                 (objc - 1 - (-2));

  return index;
}

int test2(int opnd, int objc) {
  int index;

  if ((index = opnd) <= (-2)) {
    index += (objc - 1 - (-2));
  }

  return index;
}

x64, gcc 5.1:
;#test1(int,int)
xor eax, eax
cmp edi, -1
setl al
add esi, 1
imul esi, eax
lea eax, [rsi+rdi]
ret

;#test2(int,int)
lea edx, [rdi+1+rsi]
mov eax, edi
cmp edi, -2
cmovle eax, edx
ret

x64, gcc 7.3:
;#test1(int,int)
xor eax, eax
cmp edi, -1
setl al
add esi, 1
imul eax, esi
add eax, edi
ret

;#test2(int,int)
lea ecx, [rdi+1+rsi]
mov eax, edi
cmp edi, -1
cmovl eax, ecx
ret

x64, gcc (trunk):
;#test1(int,int)
add esi, 1
mov eax, 0
cmp edi, -1
cmovge esi, eax
lea eax, [rsi+rdi]
ret

;#test2(int,int)
mov eax, edi
cmp edi, -1
lea ecx, [rdi+1+rsi]
cmovl eax, ecx
ret

x86, gcc 5.1:
;#test1(int,int)
mov ecx, [esp+4]
mov edx, [esp+8]
xor eax, eax
cmp ecx, -1
setl al
add edx, 1
imul eax, edx
add eax, ecx
ret

;#test2(int,int)
mov eax, [esp+4]
mov edx, [esp+8]
lea edx, [eax+1+edx]
cmp eax, -2
cmovle eax, edx
ret

x86, gcc 7.3:
;#test1(int,int)
mov ecx, [esp+4]
mov edx, [esp+8]
xor eax, eax
cmp ecx, -1
setl al
add edx, 1
imul eax, edx
add eax, ecx
ret

;#test2(int,int)
mov eax, [esp+4]
mov edx, [esp+8]
lea edx, [eax+1+edx]
cmp eax, -1
cmovl eax, edx
ret

x86, gcc (trunk):
;#test1(int,int)
mov edx, [esp+4]
mov eax, [esp+8]
mov ecx, 0
add eax, 1
cmp edx, -1
cmovge eax, ecx
add eax, edx
ret

;#test2(int,int)
mov eax, [esp+4]
mov edx, [esp+8]
cmp eax, -1
lea edx, [eax+1+edx]
cmovl eax, edx
ret


Наглядней (с подсветкой и т.д.) это можно увидеть тут или тут.

Что имеем в остатке:

  • во всех вариантах (и даже с максимальной оптимизацией) test1 ничем не лучше а то и проигрывает test2
  • test2 реализуется компилятором совсем без условного перехода, как можно было подумать, например просто используя условную cmovl
  • даже нативный байт-код test2 более читабелен
  • байт-код test2 короче, местами более удобен для ILP/SEP, меньше размывает CPU-кэш (как минимум instruction cache).

Тут кстати наглядно виден и некоторый прогресс в развитии компилятора.

Ну и в полном виде всей функции компилятор немного изменил весь flow, что на некоторых искусственных тестах производительности показало падение до одного — двух процентов (может отличатся для других компиляторов/систем/платформ и т.д.).

Общий же тест исполнения компилированного TCL-кода (без паразитной нагрузки) показывает незначительное падение (доли процента), что может быть просто связано с флуктуациями энергии единицы объёма вакуума.

Теперь почему как мне кажется не нужно заниматся такой «слепой» оптимизацией в уме:

  • смешивается в кучу математика и собственно flow процесса, что например усложняет разбор его для компилятора
  • при оптимизации в процессе компиляции у математики ветвление немного слабее чем у того же flow-дерева и inlining-процессов, нужен контроль за overflow, неявным поведением и т.д. (так сложилось исторически, грубо говоря у математики немного смещенный фокус при оптимизации оной).
  • исходный код становится неявным, как минимум тормозит review и анализ (иногда просто очень трудночитаем).

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

Я не говорю, что не нужно оптимизировать совсем, но если делать это, то вооружившись лупой, поглядывая в нативный результат компиляции, и выполняя замеры скорости выполнения, профайлинг производительности (как на конкретной функции, так и всего кода целиком) и т.п.
Я часто видел как некоторая оптимизация, однозначно вроде бы улучшающая байт-код (меньше и короче инструкции, оптимальная загрузка регистров, и т.д.) на несколько процентов просаживает исполнение его на реальных тестах многопоточно и/или под параллельной паразитной нагрузкой.

P.S. Всех девчонок с праздником!

За сим к опросам…

Комментарии (48)


  1. MechanicZelenyy
    08.03.2018 19:13
    -1

    Да, совсем недавно коллега сталкивался с подобным случаем, когда убрав из куска кода (написанного под компиляторы ещё плохо умеющие оптимизировать код) различные оптимизации, получил прирост производительности в 20%.


  1. sand14
    08.03.2018 21:31
    -2

    И всё как бы ничего, однако меня всё же насторожила такая казалось бы пустячная «оптимизация» с уклоном в арифметику.

    Помимо того, что не следует смешивать разные блоки кода, и операторы ветвления в один блок, используя арифметические псевдооптимизации — понятно, что это общее правило, и долго объяснять, почему это так — конкретно в этом коде серьезный залет, который не приводит ни к какой оптимизации, а только вредит.


    В исходном коде были оператор ветвления и сложение, а в новом… еще и умножение.


    (На этом можно и закончить, и не говорить о таких пустяках, что в новом коде почему то и операторов сложения больше, и операторы вычитания появились...)


    1. sebres Автор
      08.03.2018 21:37
      -1

      операторы сложения? где? между константами? учить мат-часть…


      1. sand14
        08.03.2018 21:45
        +1

        Уважаемый, в приведенном вами коде

        index = opnd + (opnd <= TCL_INDEX_END)*(objc - 1 - TCL_INDEX_END);

        нет оператора сложения (как и умножение, так и вычитания) между константами, кроме «1 — TCL_INDEX_END»

        Надеюсь, не будете выкручиваться, утверждая, что opnd и objc — константы.

        Так что учите матчасть сами.


        1. sebres Автор
          08.03.2018 22:01

          там у вас не стояло слово «лишний“ разве? если нет, тогда звиняйте (уже перебрал значит)…
          Только умножение он хотел вместо условного перехода (то что вы назвали „оператор ветвления“), так что — ваше „ещё и“ все равно мимо.


          1. khim
            09.03.2018 17:09
            +1

            Проблема в том, что усножение — весьма дорогая операция. А ветвление часто можно на cmov заменить.

            На старых процессорах без cmov оптимизация не имеет смысла. На новых процессорах с cmov — тем более.

            Так где оно может, хотя бы теоретически, иметь смысл???


            1. sebres Автор
              09.03.2018 17:42

              Проблема в том, что умножение — весьма дорогая операция.

              Да ну? Тот же imul в интелях всего с максимально 3 cycle latency если мне не изменяет память.
              А условный переход, он как привило длиннее (в реальности).


              И потом у ув. профессора Портера здесь было умножение на 0/1, т.е. как-раз как бы с надеждой заменить при компиляции на что-нибудь более подходящее.


              Просто он забыл, что компиляторы давно и буквально следуют первому правилу при процессорной оптимизации (машинного кода):


              Always avoid a branch if you can.

              Т.е. что условный переход на таком коротком if тоже не получится...


              Ну и еще забылось что имеем: branch prediction / ILP, instruction reordering, instruction, TLB and/or data cache (hits/misses/page faults) т.д., и register renaming и прочее впридачу.
              Работать компилятором нынче очень сложно. :)


              Так где оно может, хотя бы теоретически, иметь смысл???

              Я видел вполне себе хорошую реализацию switch на imul для коротких выравненных кусков кода. Даже очень. Побило всё остальное, вплоть до многоуровневых jump-tables, и прочие дериваты.
              Другое дело что оно, так-то, не универсально конечно.


              И не забывайте, что кэш процессора ограничен. Часто Ofast оптимизация, создающая код чуть длиннее чем O2, много быстрее на искусственных тестах, пока в бою не проявляются проблемы на multithreading, паразитной нагрузке и т.п.


              1. khim
                09.03.2018 18:43

                Да ну? Тот же imul в интелях всего с максимально 3 cycle latency если мне не изменяет память.
                Изменяет. Pentium — 9/11, более старые процессоры — ещё медленнее. И даже на Skylake может до 4 циклов занимать.

                Но с учётом того, что на процессорах новее Pentium Pro есть cmov критическое число — 9.

                И потом у ув. профессора Портера здесь было умножение на 0/1, т.е. как-раз как бы с надеждой заменить при компиляции на что-нибудь более подходящее.
                Напишите тогда уж XOR, тогда есть шансы.

                Просто он забыл, что компиляторы давно и буквально следуют первому правилу при процессорной оптимизации (машинного кода):
                Always avoid a branch if you can.
                Давно, но не буквально. Это мы уже обсуждали.

                Я видел вполне себе хорошую реализацию switch на imul для коротких выравненных кусков кода. Даже очень. Побило всё остальное, вплоть до многоуровневых jump-tables, и прочие дериваты.
                Я, наверное, ничего не понимаю в колбасных обрезках, но в подобных случаях ведь умножать нужно на константу, а не на переменную… то есть это lea, а не imul… сложение, а не умножение…


                1. sebres Автор
                  09.03.2018 20:22

                  > И даже на Skylake может до 4 циклов занимать.

                  2-operand form, при умножении на 0/1? Вы серьезно?

                  > Я, наверное, ничего не понимаю в колбасных обрезках

                  Наверное… :) выше под умножением понималось умножение в сишном коде, как его уже реализует компилятор не очень интересно.

                  Конкретно в контексте того упомянутого `switch`, ужо не упомнить (а думать некогда), но чисто для примера:
                  ```asm
                  ; в EAX передается значение для switch-а…
                  ; дефоулт всё что больше 50, размер каждой ветки (паддинг) = 24
                  cmp EAX, 50
                  ja switch_default
                  imul EAX, const_padding_size ;# EAX *= 24
                  jmp EAX
                  sw0th:
                  ...; instr (24 bytes)
                  sw1st:
                  ...; instr (24 bytes)
                  sw2nd:
                  ...; instr (24 bytes)

                  sw50th:
                  ...; instr (24 bytes)
                  switch_default:
                  ret
                  ```


                  1. khim
                    09.03.2018 21:18

                    2-operand form, при умножении на 0/1? Вы серьезно?
                    При умножении на 0/1 — скорее всего нет. Я не знаю как оно у них зависит от аргументов. У Agnerа таблицы не настолько детализованы.

                    Наверное… :) выше под умножением понималось умножение в сишном коде, как его уже реализует компилятор не очень интересно.
                    Не очень понятно как сишный код сочетать с «короткими выравненными кусками кода» — это уже явно ассемблерный уровень.

                    imul EAX, const_padding_size ;# EAX *= 24
                    Тут я бы два LEA поставил. Или LEA + SAL, как компилятор делает:
                    $ cat test.c
                    int foo(int x) {
                      return x*24;
                    }
                    $ gcc test.c -march=native -O3 -S -o-
                    	.file	"test.c"
                    	.text
                    	.p2align 4,,15
                    	.globl	foo
                    	.type	foo, @function
                    foo:
                    .LFB0:
                    	.cfi_startproc
                    	leal	(%rdi,%rdi,2), %eax
                    	sall	$3, %eax
                    	ret
                    	.cfi_endproc
                    .LFE0:
                    	.size	foo, .-foo
                    	.ident	"GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516"
                    	.section	.note.GNU-stack,"",@progbits
                    
                    Тут та же самая петрушка, что и с переходами. Не всё то, что выглядит как умнодение в C является умножением на ассемблере.

                    P.S. А вообще ваш трюк, как и многое подобное, не нов и не уникален. GCC старых версий подобное творит. С lea, конечно, не с imul. Впрочем последние версии не заморачиваются


  1. Cheater
    08.03.2018 22:11
    -1

    > int index;
    > if ((index = opnd) <= (-2)) {

    В чём проблема инициализировать index сразу значением opnd в примере test2? (int index = opnd) Вы приводите аргументы против смешивания математики и flow в одном выражении, но при этом именно это и делаете в test2.


    1. sebres Автор
      08.03.2018 22:36
      -1

      В чём проблема инициализировать index сразу значением opnd

      Как раз это компилятору все равно, а в такой форме оно нагляднее, и речь тут не о том… Да и в оригинальной функции оно не так...


      но при этом именно это и делаете в test2.

      Совершенно нет, и при "ассемблерном мышлении" как раз проще — результат присвоения в eax, потом cmp eax, и условное действие (переход или присвоение-пересылка).


      Все дело в том, что условный переход действие не дешевое, и раньше их часто заменяли, умножением в том числе...


  1. potan
    08.03.2018 22:38

    что там компилятор наоптимизировал, выходит иногда боком при исполнении на современном железе с собственной оптимизацией внутри, типа внеочередное или спекулятивное исполнение, параллелизм на уровне команд (ILP) и прочее.

    Опция -m arch=686 должна в этом случае помогать.


  1. smer44
    08.03.2018 23:03

    количество комманд ассемблера не показатель, напиши этот код в большом цикле и посмотри что быстрее, мне например не нравится умножение как дорогая операция, я бы сделал

    index = opnd + (~((opnd<-1)-1))&(1 + objc)


    1. smer44
      08.03.2018 23:16

      короче быстрее всех просто index = (opnd < -1)? opnd: opnd+1+indexEnd;
      вариант с магией битов 2 место, с умножением — 3 место


      1. demimurych
        08.03.2018 23:40

        условный переход всегда медленней умножения. Иногда медленнее на порядки если процессор неправильно предскажет ветвление.


        1. smer44
          09.03.2018 00:28

          да вроде нет, цикл на 100 миллионов операций 10% быстрее работает паскуда, то есть ещё несколько быстрее за вычетом других комманд, а фокус с битами на 3-5% быстрее умножения, или я запускаю как то не так


          1. tyomitch
            09.03.2018 14:59

            Разница будет зависеть от целевой архитектуры, используемого компилятора, опций компиляции, конкретного процессора, и т.д. и т.п. — нельзя сказать, что «ветвление однозначно быстрее битовых фокусов» или наоборот.


        1. khim
          09.03.2018 18:51

          Вообще нет. Правильно предсказанный переход — это вообще 0 тактов. Вот неправильно предсказанный — дело другое.

          Но при этом нужно учитывать, что условный оператор != условный переход.


  1. Gumanoid
    08.03.2018 23:46

    А разве результат сравнения True гарантированно будет равен 1? Я думал там может получиться любое ненулевое число.


    1. fshp
      09.03.2018 00:55

      В C нет типа Boolean. Типом результата сравнения является int.


      The == (equal to) and != (not equal to) operators are analogous to the relational
      operators except for their lower precedence.108) Each of the operators yields 1 if the
      specified relation is true and 0 if it is false. The result has type int. For any pair of
      operands, exactly one of the relations is true.


      http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
      Страница 96.


      1. fshp
        09.03.2018 00:57

        Конструкция "if(foo)" равносильна "if(foo!=0)". Именно поэтому любое ненулевое выражение является истинным.


  1. bsivko
    09.03.2018 00:01

    Сталкивался с подобными эффектами при оптимизации ещё лет 10 назад и с того времени отношусь с осторожностью. Но такого как вы анализа не делал, спасибо за статью.


    1. sebres Автор
      09.03.2018 00:14

      да разве то анализ… то просто один коммит, по моему неплохо подходящий в качестве примера


  1. Bonart
    09.03.2018 02:35
    +1

    По этой причине эта функция еще и самая большая (порядка 6 тысяч строк + примитивы и макросы)

    Сдается мне, не в оптимизациях там проблема.


    1. ser-mk
      09.03.2018 02:48
      +1

      Так не только вам сдается.


  1. u_story
    09.03.2018 08:52
    +2

    А использование -O3 и -Os как-нибудь повлияет на результат?
    А можно другие компиляторы clang и VS так же себя ведут?


    1. tyomitch
      09.03.2018 13:28
      +2

      Под clang код test1 и test2 получается одинаковым.
      Под gcc для AArch64 тоже получается одинаковым.
      В общем, не настолько компиляторы глупы, как sebres их пытается выставить.
      Главным аргументом при выборе между двумя вариантами должна быть читаемость кода, а не то, в какую последовательность команд он оттранслируется конкретной версией компилятора с конкретным набором флагов.


      1. sebres Автор
        09.03.2018 14:29

        не настолько компиляторы глупы, как sebres их пытается выставить.

        А нельзя ткнуть носом, где я пытаюсь их такими выставить?


        Наоборот я их всегда, всячески хвалю, например — "Не стоит забывать про оптимизацию в современных компиляторах, которая достигла если не совершенства, то уже очень и очень на уровне."


        И потом я же написал — "во всех вариантах (и даже с максимальной оптимизацией) test1 ничем не лучше, а то и проигрывает test2".


        Про собственно clang же много историй можно рассказывать. Он у меня был любимый долгое время… даже кастомная собственная сборка с измененным поведением при оптимизации.
        Потом я столько всего под clang словил, что окончательно забросил...


        1. tyomitch
          09.03.2018 14:48

          Я имел в виду:

          смешивается в кучу математика и собственно flow процесса, что например усложняет разбор его для компилятора

          --здесь уместнее было написать «усложнял», потому что более навороченным компиляторам уже и такой смешанный код по зубам.


          1. sebres Автор
            09.03.2018 14:59

            Здесь конкретно да, а в других примерах ещё как посмотреть...


            Про "более навороченным компиляторам" тоже могу поспорить.
            Оно местами настолько "навороченно", что неявное (а то и неопределённое) поведение на нормальном течении словить можно (было как-то, долго боролся, помог откат на пред. версию).


            Из последнего вот к примеру, expressive diagnostics у них вообще таким местом реализован, что варнинг на варнинге и третьим погоняет (даже без "-Wextra"). А если например в проекте варнинг к ошибке преравнивается?


  1. sshmakov
    09.03.2018 09:02

    Интересно, а если убрать index, который здесь совсем не нужен, а делать все операции над передаваемым параметром opnd, то результат оптимизации изменится?


  1. Danik-ik
    09.03.2018 10:03
    +11

    А мне кажется, что самое неправильное в данном коммите — полное уничтожение читаемости. До коммита — ясно, что, почему и как, а после — сим-салябим, ахалай махалай, тыдыщ — ЧУДО!
    Даже если бы оно работало быстрее, сокращение комментария вместо добавления обоснования применённого метода — чистой воды вредительство.


    1. Danik-ik
      09.03.2018 10:06
      +6

      То есть в тексте о б этом сказано, но мимоходом, и на самом последнем месте. А по мне — надо на первом.


  1. vscrub
    09.03.2018 10:19

    Я могу ошибаться, но 3 операции с памятью в test2 могут выполняться дольше двух в test1. И, следовательно, test2 будет медленнее, несмотря на то, что короче.
    Поправьте меня если я ошибся.


    1. arteast
      09.03.2018 11:02
      +1

      Поправляю — там вообще нет операций с памятью, кроме загрузки значений операндов со стека (которые одинаковы в обоих вариантах). lea — не операция с памятью, несмотря на вид ее второго параметра.


  1. Scf
    09.03.2018 11:22
    +3

    Не хватает варианта "не занимаюсь пессимизацией кода прямо в процессе разработки".
    Я за алгоритмическую оптимизацию — если есть возможность сделать из квадратичной сложности линейную или из линейной логарифмическую — почему нет?


    Важно только помнить, что логарифмическая сложность не всегда быстрее линейной :-)


  1. novikovag
    09.03.2018 12:18
    -1

    А как же мохнатое О люто форсимое смузихлебами оторванными от реальности?


    1. bolk
      09.03.2018 18:02
      -1

      «Мохнатое О» не имеет отношения к смузи. У нас в университете (КФУ, ВМК) это в 90-х на первом курсе читали. Покойный Самитов сейчас в гробу вертится от вашего «смузихлёбства».


      1. novikovag
        10.03.2018 07:35
        -4

        Лучше бы вас там программы писать учили, а не бесполезным фантастическим теориям.


        1. bolk
          10.03.2018 07:48
          +4

          Вы это серьёзно что ли? О-большое — необходимая часть для понимания сложности алгоритмов и написания программ.


          1. novikovag
            10.03.2018 08:48
            -4

            К реальному миру не имеющее ни какого отношения, топик тому пример.


            1. khim
              10.03.2018 18:19
              +1

              Топик описывает что нужно делать после того, как с «big O» всё в порядке.

              Сколько я видел «сурово оптимизированных» программ с пресловутым алгоритмом маляра Шлемиэля… Не сосчитать. И они все легко «обходятся» совершенно неоптимизированным кодом, в котором пресловутого «Шлемиэля» нету.

              А вот если вы всех «Шлемиэлей» извели — тогда уже можно и о тактах думать.


  1. CryptoPirate
    09.03.2018 12:18
    +4

    Не думаю, что в статье пример из этой области, но есть небольшая тонкость по поводу «убрать условные переходы». В криптографических алгоритмах стараются избегать всех условных переходов которые зависят от используемых данных (просто условный переход в цикле не в счёт). В таких случаях к чёрту оптимизацию в пару циклов, там важнее чтобы не было атак по сторонним каналам (по времени, например). Так что, в некоторых отдельных случаях такой подход полезен.


  1. mike_y_k
    09.03.2018 14:32

    Таки каждому овощу свою грядку.
    Тут ещё про мелкие процессоры с их прошивками вспомнить с их проблемами…
    Но в примере во первых отсутствие внятного комментария и обоснования применённого решения больше всего напрягает изначально.
    За такие включения даже только для себя любимого стоит по рукам и по голове…


  1. caway
    09.03.2018 16:38

    Данные манипуляции лишь запутывают компилятор, не давая ему сделать качественную оптимизацию.


  1. alexeibs
    09.03.2018 19:42

    clang, начиная с версии 3.7, оба варианта компилирует одинаково.


    1. khim
      09.03.2018 21:26

      При этом версия 3.6 лучше компилирует как раз «однозначно более быстрый вариант»… Так что разрабатывай вы это всё под Mac несколько лет назад — был бы повод добавить комментариев и эту версии принять.

      Впрочем есть ощущение, что сегодня, сейчас, закладываться на более старую версию clang'а не стоит — тем более, что и icc и MSVC лучше обрабатывают test2.