Держи багОбщаясь с людьми на конференциях и в комментариях к статьям, мы сталкиваемся со следующим возражением: статический анализ сокращает время на нахождение ошибок, но отнимает время у программистов, что нивелирует пользу от его использования и даже наоборот тормозит процесс разработки. Давайте разберём это возражение и покажем, что оно беспочвенно.

Утверждение «статический анализ будет отнимать часть рабочего времени» в отрыве от контекста является верным. Регулярный просмотр предупреждений статического анализатора, выдаваемых на новый или изменённый код, действительно отнимает время. Однако следует продолжить мысль: но затрачиваемое на это время гораздо меньше, чем потраченное на выявление ошибок другими методами. Ещё хуже — узнавать об ошибках от клиентов.

Здесь очень хорошей аналогией могут являться юнит-тесты. Юнит-тесты точно также отнимают время разработчиков, однако это не является причиной их не использовать. Польза от написания более качественного и проверенного кода при использовании юнит-тестов превосходит затраты на их написание.

Ещё одна аналогия: предупреждения компилятора. Это вообще очень близкая тема, так как предупреждения инструментов статического анализа в первом приближении можно рассматривать как расширение предупреждений компиляторов. Естественно, когда программист видит предупреждение компилятора, он тратит на него время. Он должен или изменить код, или явно потратить время на подавление предупреждения, например, с помощью #pragma. Однако эти затраты времени не являются причиной отключить предупреждения компилятора. А если кто-то так сделает, это будет однозначно интерпретироваться другими как профессиональная непригодность.

Тем не менее, откуда же берёт начало испуг перед необходимостью тратить время на предупреждения статических анализаторов кода?

Всё очень просто. Программисты, ещё мало знакомые с этой методологией, путают пробные запуски и регулярное использование. При первых запусках любые анализаторы выдают огромный список предупреждений, на который даже страшно смотреть. Причина в том, что анализатор ещё не настроен. Настроенный же анализатор выдаёт при регулярных запусках малое число ложных срабатываний. Другими словами, большинство предупреждений выявляют реальные дефекты или код с запахом. Важно только сделать эту настройку. В этом и есть весь фокус, который превращает статический анализатор из зла, отнимающего время, в друга и помощника.

Любой статический анализатор вначале выдаст много ложных срабатываний. Причин тому много, и эта тема заслуживает отдельной статьи. Естественно, и мы, и разработчики других анализаторов борются с ложными срабатываниями. Но срабатываний всё равно будет много, если без подготовки вдруг взять и запустить анализатор на каком-то проекте. Такая же картина, кстати, и с предупреждениями компилятора. Пусть у вас есть большой проект, который вы всегда собирали, например, с помощью компилятора Visual C++. Предположим, проект чудесным образом получился переносимым и скомпилировался с помощью GCC. Даже в этом случае вы получите гору предупреждений от GCC. Кто переживал смену компиляторов в большом проекте, тот понимает, о чём речь.

Предупреждения


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

Настройка анализаторов, как и компиляторов, не так сложна и трудозатрата, как любят пугать программисты. Если вы — менеджер, не слушайте их. Они просто ленятся. Программист с гордостью может рассказывать, как он 3 дня искал баг, найденный тестировщиком/клиентом. И это для него нормально. Однако с его точки зрения неприемлемо потратить один день на настройку инструмента, после чего подобная ошибка будет выявляться, ещё не попав в систему контроля версий.

Да, ложные срабатывания будут присутствовать и после настройки. Но их количество преувеличивается. Вполне можно настроить анализатор, чтобы процент ложных срабатываний составлял 10%-15%. Т.е. на 9 найденных дефектов только 1 предупреждение потребует подавления как ложное. Так где же здесь «трата времени»? При этом 15% — это вполне реальное значение, о чём подробнее можно прочитать в этой статье.

Остаётся ещё один момент. Программист может возразить:

Хорошо, предположим, регулярные запуски статического анализа действительно эффективны. Но что делать с первоначальным шумом? На нашем большом проекте мы не сможем настроить инструмент за 1 обещанный день. Только одна перекомпиляция, чтобы проверить очередную порцию настроек, занимает несколько часов. Мы не готовы потратить пару недель на всё это.

И это не проблема, а попытка найти повод не внедрять что-то новое. Конечно, в большом проекте всегда всё непросто. Но, во-первых, мы оказываем поддержку и помогаем интегрировать PVS-Studio в процесс разработки. А во-вторых, вовсе не обязательно начинать разбирать все предупреждения.

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

Лучше считать существующие предупреждения техническим долгом. К долгу можно будет вернуться позже и работать со старыми предупреждениями постепенно. Используя механизм массового подавления предупреждений, вы можете начать быстро использовать PVS-Studio в большом проекте. Совсем кратко это происходит так:

  1. Вы исключаете из анализа явно лишние директории (сторонние библиотеки). Эту настройку лучше в любом случае делать в самом начале, чтобы сократить время анализа.
  2. Вы пробуете PVS-Studio и изучаете самые интересные предупреждения. Вам нравятся результаты, и вы показываете инструмент коллегам и начальству. Команда решает начать его регулярное использование.
  3. Проверяется проект. Все найденные предупреждения отключаются с помощью механизма массового подавления. Другими словами, все имеющиеся на данный момент предупреждения теперь считаются техническим долгом, к которому можно вернуться позже.
  4. Получившийся файл с подавленными предупреждениями закладывается в систему контроля версий. Этот файл большой, но это не страшно. Вы делаете эту операцию один раз (или, по крайней мере, будете делать крайне редко). И теперь этот файл появится у всех разработчиков.
  5. Теперь все разработчики видят предупреждения, относящиеся только к новому или изменённому коду. С этого момента команда начинает получать пользу от статического анализа. Постепенно настраиваете анализатор и занимаетесь техническим долгом.

Отлично!

Кстати, система хранения неинтересных предупреждений достаточно умная. Хранятся хеши для строки с потенциальной ошибкой, а также для предыдущей и следующей. Благодаря этому, если в начало одного из файлов добавить строчку, то ничего «не разъедется» и анализатор по-прежнему будет молчать на код, считающийся техническим долгом.

Надеюсь, нам удалось развеять одно из предубеждений относительно статического анализа. Приходите, скачивайте и пробуйте наш статический анализатор кода PVS-Studio. Он будет выявлять множество ошибок на ранних этапах и сделает ваш код в целом более надёжным и качественным.

Примечание

При разработке любого проекта постоянно появляются и исправляются новые ошибки. Ненайденные ошибки «оседают» в коде надолго, и затем многие из них могут быть выявлены при запуске статического анализа кода. Из-за этого иногда возникает ложное ощущение, что статические анализаторы находят только какие-то малоинтересные ошибки в редко используемых участках кода. Конечно, так оно и есть, если использовать анализатор неправильно и запускать его только время от времени, например, незадолго до выпуска релиза. Подробнее эта тема разбирается здесь. Да, мы сами при написании статей выполняем разовые проверки открытых проектов. Но у нас другая цель. Мы хотим продемонстрировать возможности анализатора кода по выявлению дефектов. Эта задача имеет мало общего с повышением качества кода проекта в целом и сокращением издержек, связанных с правкой ошибок.

Дополнительные ссылки:

  1. PVS-Studio ROI.
  2. Статический анализ улучшит кодовую базу сложных C++ проектов.
  3. Heisenbug 2019. Доклад Ивана Пономарёва "Непрерывный статический анализ кода".
  4. Иван Пономарев. Внедряйте статический анализ в процесс, а не ищите с его помощью баги.



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Handling Objections: Static Analysis Will Take up Part of Working Time.

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


  1. amarao
    12.09.2019 14:56

    Я считаю, что система, в которой варнинги надо "подавлять" является деструктивной для производственной культуры. Если стандартной реакций на варнинг является не "поправить", а "заткнуть", то люди быстро привыкают, что надо просто затыкать.


    Ситуация совершенно аналогичная мониторингу. Чем больше красных лампочек надо игнорировать, тем ниже его польза. См ядерную аварию на 3 miles island, где была именно ситуация "100 лампочек красные и их надо игнорировать, 101ая — признак аварии реактора".


    1. Andrey2008 Автор
      12.09.2019 15:09
      +1

      Я считаю, что система, в которой варнинги надо «подавлять» является деструктивной для производственной культуры.
      Вроде как мысль правильная и я с ней согласен. Но только она мимо :). Вот возьмём, например, проверки ошибок в Microsoft Word. Он тоже иногда подчёркивает не то, что нужно. Повод ли это отключить проверку текста? Нет конечно. С ней, текст будет более качественный, несмотря на ложные срабатывания.

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


    1. SirEdvin
      12.09.2019 16:13
      +1

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


      С другой стороны, шлифовать анализатор от ложных срабатываний тоже надо, но мне кажется, упрекнуть pvs-studio в том, что они этого не делают сложно.


      1. amarao
        12.09.2019 16:18

        У меня ощущение, что статический анализатор для С/С++ — это как попытка заложить прорвавшуюся плотину мешками с песком.


        UB без варнингов в стандарте языка — это всё. Можно закапывать. Никакие сторонние иструменты эту проблему уже не решат.


        1. SirEdvin
          12.09.2019 16:21
          +1

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


          Причем статические анализаторы не только по UB, они еще могут быть про все, что угодно от неочевидных мест языка до стилестических ошибок.


        1. KanuTaH
          12.09.2019 18:04
          +2

          UB — это неотъемлемая часть современной действительности, смиритесь :) В современных CPU как таковых полно UB. Например, на x86 попытка выполнить инструкцию BSF над нулевым source operand — это UB в том смысле, что содержимое destination operand может оказаться абсолютно любым, и если рассчитывать на его содержимое в дальнейшем потоке выполнения, то в конечном счете может произойти что угодно. В AVR попытка выполнить любую из следующих инструкций:

          LD r26, X+
          LD r27, X+
          LD r26, -X
          LD r27, -X

          — тоже аналогично является UB. C/C++, как языки, максимально приближенные к «железу», и максимально оптимизируемые, используют UB в своих целях, рассчитывая на то, что в корректно написанной программе не нужно будет делать на каждый чих кучу неявных runtime-проверок на NULL, на переполнение знаковых типов, на выход за границы массива, и так далее, и тому подобное. Варнинги тут не помогут просто потому, что компилятор не способен отловить такие ситуации на этапе собственно компиляции — как, например, отловить signed overflow на этапе компиляции? Да никак — только runtime checks, только хардкор. C/C++ предполагают, что разработчик позаботится об этом сам в тех местах и в той степени, в которой посчитает нужным.


          1. mayorovp
            13.09.2019 10:25

            Например, на x86 попытка выполнить инструкцию BSF над нулевым source operand — это UB в том смысле, что содержимое destination operand может оказаться абсолютно любым

            Нет, это не undefined behavior, а всего лишь unspecified. Undefined behavior тут бы бы, если бы некорректная команда, к примеру, повреждала память микропрограмм. Ну или одновременное обращение к одной и той же области памяти из разных ядер без барьеров — но это "привычное" UB, его обычно в недостатки языка не записывают.


            1. KanuTaH
              13.09.2019 10:41

              Unspecified behavior в терминах C/C++ в данном случае был бы, если бы, скажем, в зависимости от конкретной модели процессора значение destination operand было бы разным, но отнюдь не произвольным (each unspecified behavior results in one of a set of valid results). То есть, например, unspecified behavior — это операция foo() + boo(), результат которой в конце концов строго определён (результат foo() плюс результат boo()), но в каком порядке будут вызваны foo и boo — не определено. Документация Intel же никакого set of valid results не очерчивает.


              1. mayorovp
                13.09.2019 11:43

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


                UB — это когда, например, выражение i >= 0 ? i : 0 выдаёт -1.


                1. KanuTaH
                  13.09.2019 11:54

                  Ну так и в вашем случае будет "хоть какое-то значение, которое можно обработать" :) Главное отличие unspecified behavior от undefined ведь именно что в наличии описанного в документации set of valid results. Ну это опять же в терминах C/C++ если говорить, в документации от Intel своя терминология (там, кстати, прямо употребляется термин undefined).


                  1. mayorovp
                    13.09.2019 13:12

                    Ну и какое значение в моём примере у переменной i?


                    1. KanuTaH
                      13.09.2019 13:27

                      Ну у вашего выражения есть конкретный результат (-1), который можно обработать :) В общем, ладно, над вопросами перекрестного применения терминологии из разных областей можно спорить бесконечно: я считаю, что в случае BSF undefined behaviour потому, что отсутствует документированный set of valid results, а вы, очевидно, считаете, что это unspecified behaviour потому, что в регистре в конце концов будет что-то, что укладывается в его размерность (хоть это что-то и нельзя будет потом использовать каким-либо разумным образом). Пусть каждый останется при своем мнении :)


                    1. qw1
                      13.09.2019 17:58

                      Ну и какое значение в моём примере у переменной i?
                      unsigned i = 0xFFFFFFFF;
                      int result = i >= 0 ? i : 0;


                      1. mayorovp
                        13.09.2019 18:32

                        Это вы написали вариант, при котором в самом выражении i >= 0 ? i : 0 происходит UB. А я писал про вариант, когда UB уже произошло до этой строчки.


                        Например, что-то вроде такого:


                        int foo = внешняяФункцияЧтоВернётIntMax();
                        if (foo < 0) {
                            return;
                        }
                        
                        int i = foo*2 + 1;
                        if (i >= 0) {
                            printf("%d >= 0", i); // -1 >= 0
                        }

                        Можно ли считать, что в переменной i тут вообще находится хоть какое-то значение?


                        1. qw1
                          13.09.2019 20:24

                          Здесь вы смешиваете контексты. Когда говорите «значение, которое, несмотря на бессмысленность, все же можно обработать» — это я понял как о скомпилированном коде (runtime). Там всё детерминированно, а пример выше — об оптимизациях компилятора. В compile-time никакого значения ещё нет.


                          1. mayorovp
                            14.09.2019 09:21

                            это я понял как о скомпилированном коде (runtime). Там всё детерминированно

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


                            1. qw1
                              15.09.2019 10:45

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


    1. netch80
      15.09.2019 08:14

      Аналогия понятная, но некорректная. В случае «100 лампочек красные» человек легко запутается, какие лампочки надо замечать, а какие нет. В случае предупреждений анализатора с корректно заглушёнными проблема только тогда, когда они вдруг могут перейти из неважных в важные, не меняя положения в коде и содержания предупреждения. По-моему, это очень малореально.
      Кстати, если бы на том атомном блоке постоянно горящие красные лампочки закрывали заглушками — было бы аналогично: нормально не видим красных, одна появилась — ой, надо что-то делать.