Оптимизация кода в уме, или «Ну так же однозначно быстрее»

    Намедни работая над одной ошибкой в одном опенсорсном проекте, увидел как коллега (тоже работающий параллельно над той же проблемой) залил такой вот коммит [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. Всех девчонок с праздником!

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

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Занимаетесь ли вы оптимизацией вашего кода:
    Каков процесс оптимизации:
    Выделяется ли на работе бюджет на оптимизацию:
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 50
    • +3
      Да, совсем недавно коллега сталкивался с подобным случаем, когда убрав из куска кода (написанного под компиляторы ещё плохо умеющие оптимизировать код) различные оптимизации, получил прирост производительности в 20%.
      • +2
        И всё как бы ничего, однако меня всё же насторожила такая казалось бы пустячная «оптимизация» с уклоном в арифметику.

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


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


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

        • –7
          операторы сложения? где? между константами? учить мат-часть…
          • +7
            Уважаемый, в приведенном вами коде
            index = opnd + (opnd <= TCL_INDEX_END)*(objc - 1 - TCL_INDEX_END);

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

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

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

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

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

                Так где оно может, хотя бы теоретически, иметь смысл???
                • 0
                  Проблема в том, что умножение — весьма дорогая операция.

                  Да ну? Тот же 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, паразитной нагрузке и т.п.

                  • 0
                    Да ну? Тот же 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… сложение, а не умножение…
                    • 0
                      > И даже на 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
                      ```
                      • 0
                        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. Впрочем последние версии не заморачиваются
        • +3
          > int index;
          > if ((index = opnd) <= (-2)) {

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

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


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

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


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

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

            Опция -m arch=686 должна в этом случае помогать.
            • –1
              количество комманд ассемблера не показатель, напиши этот код в большом цикле и посмотри что быстрее, мне например не нравится умножение как дорогая операция, я бы сделал

              index = opnd + (~((opnd<-1)-1))&(1 + objc)
              • 0
                короче быстрее всех просто index = (opnd < -1)? opnd: opnd+1+indexEnd;
                вариант с магией битов 2 место, с умножением — 3 место
                • 0
                  условный переход всегда медленней умножения. Иногда медленнее на порядки если процессор неправильно предскажет ветвление.
                  • 0
                    да вроде нет, цикл на 100 миллионов операций 10% быстрее работает паскуда, то есть ещё несколько быстрее за вычетом других комманд, а фокус с битами на 3-5% быстрее умножения, или я запускаю как то не так
                    • 0
                      Разница будет зависеть от целевой архитектуры, используемого компилятора, опций компиляции, конкретного процессора, и т.д. и т.п. — нельзя сказать, что «ветвление однозначно быстрее битовых фокусов» или наоборот.
                    • –1
                      Вообще нет. Правильно предсказанный переход — это вообще 0 тактов. Вот неправильно предсказанный — дело другое.

                      Но при этом нужно учитывать, что условный оператор != условный переход.
                • +1
                  А разве результат сравнения True гарантированно будет равен 1? Я думал там может получиться любое ненулевое число.
                  • +1

                    В 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

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

                  • 0
                    Сталкивался с подобными эффектами при оптимизации ещё лет 10 назад и с того времени отношусь с осторожностью. Но такого как вы анализа не делал, спасибо за статью.
                    • 0
                      да разве то анализ… то просто один коммит, по моему неплохо подходящий в качестве примера
                    • +8
                      По этой причине эта функция еще и самая большая (порядка 6 тысяч строк + примитивы и макросы)

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

                      • +2
                        Так не только вам сдается.
                      • +2
                        А использование -O3 и -Os как-нибудь повлияет на результат?
                        А можно другие компиляторы clang и VS так же себя ведут?
                        • +2
                          Под clang код test1 и test2 получается одинаковым.
                          Под gcc для AArch64 тоже получается одинаковым.
                          В общем, не настолько компиляторы глупы, как sebres их пытается выставить.
                          Главным аргументом при выборе между двумя вариантами должна быть читаемость кода, а не то, в какую последовательность команд он оттранслируется конкретной версией компилятора с конкретным набором флагов.
                          • 0
                            не настолько компиляторы глупы, как sebres их пытается выставить.

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


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


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


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

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

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

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


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


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

                        • 0

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

                          • +12

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

                            • +7

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

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

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

                              • +3

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


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

                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • +2
                                    «Мохнатое О» не имеет отношения к смузи. У нас в университете (КФУ, ВМК) это в 90-х на первом курсе читали. Покойный Самитов сейчас в гробу вертится от вашего «смузихлёбства».
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • +6
                                        Вы это серьёзно что ли? О-большое — необходимая часть для понимания сложности алгоритмов и написания программ.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          • +1
                                            Топик описывает что нужно делать после того, как с «big O» всё в порядке.

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

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

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

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

                                            Впрочем есть ощущение, что сегодня, сейчас, закладываться на более старую версию clang'а не стоит — тем более, что и icc и MSVC лучше обрабатывают test2.
                                          • –1
                                            В общем кто-то в очередной раз подумал, что он умнее компилятора, с предсказуемым результатом (и бонусом в виде обсуфкации кода).
                                            • 0
                                              Кто-то — это на минуточку профессор (в UNC)..., про «умнее» компилятора — это думаю однозначно (по крайней мере Дон уж точно их не одну тонну перелопатил вдоль и поперёк)..., сделал он этот «фикс» походя (не сильно напрягаясь сам, и вряд ли напрягая команду — ибо конкретно такие «обфускации» даже новички читают бегло)..., и результат вовсе не однозначно предсказуем/очевиден.
                                              Т.е. напрасно — да, ИМХО возможно даже показательно (в качестве примера, отсюда статья).
                                              Однако это писалось однозначно не для того, что бы кого-то тыкать носом.

                                              В общем кто-то мог просто пройти мимо, не оставляя таких «капитанских» и очень содержательных комментариев.
                                              П.С. Ну и наверно стоит всё же немного уважительней относится к людям, тратящим своё время и нервы на опенсорс…

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

                                            Самое читаемое