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

TensorFlow

TensorFlow - библиотека машинного обучения, разработанная корпорацией Google и доступная как проект с открытым исходным кодом с 9 ноября 2015 года. На данный момент он активно используется в исследовательской работе и в десятках коммерческих продуктов Google, включая Google Search, Gmail, YouTube, Photos, Translate, Assistant и др. Исходный код доступен в репозитории на GitHub и на сайте Платформа Google Open Source.

Почему был выбран этот проект?

  • Google. Если проект разрабатывается Google, Microsoft или любыми другими известными разработчиками, его анализ - это своего рода вызов для нас. Кроме того, многим было бы интересно увидеть ошибки разработчиков из крупных корпораций.
  • Машинное обучение. В наше время эта тема приобретает все большую популярность. Не зря некоторые результаты действительно впечатляют! Я не буду приводить здесь примеры, вы легко можете сами их найти.
  • Статистика на GitHub. Это тоже довольно важный критерий, ведь чем популярнее проект, тем лучше. TensorFlow бьет все возможные и невозможные рекорды! Занимает одно из первых мест среди C ++ проектов, имеет более 50 000 звезд и более 20 000 форков! Это потрясающе!

Конечно, мы не можем упустить шанс проверить такой проект. Даже не знаю, почему мои коллеги еще не проверили. Что ж, пора сделать это.

Что было инструментом анализа?

Если вы знаете, что такое PVS-Studio, то знаете ответ. Если вы все еще не в курсе, не спешите читать. Например, было бы интересно узнать, что у нас есть анализатор C # более года, а Linux-версия примерно на полгода.

Вот также общая информация об инструменте. Анализ проводился с помощью статического анализатора кода PVS-Studio, который находит ошибки в программах, написанных на языках C, C ++ и C #. PVS-Studio работает под Linux и Windows; На данный момент в нем более 400 диагностик, описание которых вы можете найти на этой странице.

Помимо разработки статического анализатора, мы также проверяем проекты с открытым кодом и пишем отчеты о результатах. На данный момент мы проверили более 280 проектов, в которых нашли более 10 800 ошибок. Это не какие-то мелкие и незначительные проекты, а вполне известные: Chromium, Clang, GCC, Roslyn, FreeBSD, Unreal Engine, Mono и другие.

PVS-Studio доступен для скачивания, поэтому предлагаю опробовать его на своем проекте и проверить, что он может найти в вашем коде.

Кстати, у PVS-Studio есть свой тег на StackOverflow (ссылка). Рекомендую задавать вопросы там, чтобы другие разработчики могли быстро найти необходимую информацию, не дожидаясь нашего ответа по электронной почте. В свою очередь, мы всегда рады помочь нашим пользователям.

Формат статьи

На этот раз хочу отвлечься от традиционного потока анализа: скачал проект - проверил - написал о найденных багах. Также хочу рассказать о некоторых настройках анализатора и о том, чем они могут быть полезны. В частности, я покажу, как бороться с ложными срабатываниями, как извлечь выгоду из отключения определенных диагностик и исключения определенных файлов из анализа. Конечно, мы рассмотрим ошибки, которые PVS-Studio удалось найти в исходном коде TensorFlow.

Подготовка к анализу

Теперь, когда PVS-Studio доступен и под Linux, у нас есть выбор, как проводить анализ: под Linux или Windows. Совсем недавно я проверил один проект под openSUSE, который оказался довольно простым и удобным, но все же я решил проверить TensorFlow под Windows. Мне это было более знакомо. Более того, его можно собрать с помощью CMake, что предполагает дальнейшую работу в среде Visual Studio IDE, для которой у нас есть специальный плагин (в последней версии добавлена ​​подсветка кода ошибочных фрагментов).

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

В результате мы получаем набор файлов .vcxproj, объединенных в один .sln,, а это значит, что в дальнейшем с проектом из Visual Studio будет комфортно работать, что здорово. Я работал из среды разработки Visual Studio 2017, поддержка которой была добавлена ​​в PVS-Studio 6.14 release.

Примечание. Перед анализом рекомендуется создать проект и убедиться, что он скомпилирован и в нем нет ошибок. Необходимо заверить, что анализ будет выполнен качественно и анализатор будет располагать всей синтаксической и семантической информацией. На сайте TensorFlow теперь есть примечание: По умолчанию сборка TensorFlow из исходных текстов требует много оперативной памяти. Ничего страшного, потому что у меня на машине 16 ГБ ОЗУ. Что вы думаете? Во время сборки у меня была Неустранимая ошибка C1060 (компилятору не хватает места в куче)! Моя машина забрала нашу память! Это было довольно неожиданно. Нет, у меня не было пяти виртуальных машин, работающих одновременно со сборкой. Справедливости ради следует добавить, что, используя bazel для сборки, вы можете ограничить количество используемой оперативной памяти (описание приведено в инструкциях по сборке TensorFlow).

Мне не терпелось нажать на священную кнопку «Анализировать решение с помощью PVS-Studio» и увидеть те интересные ошибки, которые мы обнаружили, но сначала было бы хорошо исключить из анализа те файлы, которые не представляют особого интереса: сторонние библиотеки , Например. Это легко сделать в настройках PVS-Studio: на вкладке «Не проверять файлы» мы устанавливаем маски тех файлов и путей, анализ которых не представляет интереса. В настройках уже есть определенный набор путей (например / boost /). Пополнил двумя масками: / third_party / и / external /. Это позволяет не только исключать предупреждения из окна вывода, но и исключать файлы каталогов из анализа, что сокращает время анализа.

Рисунок 1. Настройка анализа исключений в настройках PVS-Studio

Наконец, мы можем запустить анализ и посмотреть, что было найдено.

Примечание. «Не проверять файлы» можно настроить до и после анализа. Я только что описал первый случай, второй сценарий позволяет фильтровать полученный журнал, что тоже полезно и может избавить вас от просмотра ненужных предупреждений. Это будет описано ниже.

Ложные срабатывания: арифметика и развлечения

Почему ложные срабатывания важны (и разочаровывают)

Ложные срабатывания - головная боль для всех: и для нас, разработчиков статического анализатора кода, и для пользователей, потому что они загромождают полезный вывод. Большое количество ложных срабатываний может оттолкнуть людей от использования инструмента. Кроме того, люди обычно судят об анализаторе по критерию процента ложных срабатываний. Это не так просто, как может показаться, и эта тема для отдельной статьи и обсуждения. Мой коллега недавно написал статью об этом, рекомендую взглянуть на нее.

Как бороться с ложными срабатываниями?

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

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

Ложные срабатывания и TensorFlow

Почему я вообще заговорил о ложных срабатываниях? Во-первых, потому что очень важно бороться с ложными срабатываниями, во-вторых, из-за того, что я увидел, когда проверял TensorFlow и фильтровал и выводил по диагностическому правилу V654 (изображение кликабельно).

Рисунок 2. Все обнаруженные предупреждения V654 имеют одинаковый шаблон

64 предупреждения, и все они имеют одинаковый шаблон - следующий код:

false && expr

В самом коде эти фрагменты выглядят так:

DCHECK(v);
DCHECK(stream != nullptr);
DCHECK(result != nullptr);

Вот как объявляется макрос DCHECK:

#ifndef NDEBUG
....
#define DCHECK(condition) CHECK(condition)
....
#else
....
#define DCHECK(condition) \
  while (false && (condition)) LOG(FATAL)
....
#endif

Что следует из этого кода? DCHECK - отладочный макрос. В отладочной версии он расширен до проверки условия (CHECK (condition)), в релизной версии - до цикла, который никогда не будет выполнен - ​​while (false &&…. ). Поскольку я создавал окончательную версию кода, макрос расширился соответственно (до цикла while). В результате анализатор, кажется, жалуется правильно - потому что результат выражения всегда false. Но какой смысл в этих предупреждениях, если они выдаются для кода, который должен был быть таким? Таким образом, процент ложных срабатываний для этой диагностики будет таким же, как на диаграмме ниже.

Рисунок 3 - Соотношение хороших и ложных срабатываний диагностики V654

Вы могли подумать, что это шутка? Нет, не шутим, ложных срабатываний 100%. Именно об этом я и говорил. Я также сказал, что есть разные способы борьбы с ними. Нажав «Добавить выбранные сообщения в базу подавления», мы можем исправить это в обратном направлении (изображение кликабельно).

Рисунок 4. Борьба с ложными срабатываниями

Это способ подавить все текущие предупреждения, удалив их из окна вывода. Но это не совсем правильно, потому что если вы снова начнете использовать макрос DCHECK при написании нового кода, вы снова получите предупреждения. Выход есть. Нам нужно подавить предупреждение в макросе, оставив специальный комментарий. Тогда код подавления будет следующим:

//-V:DCHECK:654 
#define DCHECK(condition) \
  while (false && (condition)) LOG(FATAL)

Комментарий должен быть записан в том же заголовочном файле, где объявлен макрос.

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

Рисунок 5. Мы успешно исключили ложные срабатывания

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

Еще пара настроек

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

Предупреждения в автоматически сгенерированных файлах

В ходе анализа мы проверили не только код, который был написан программистами вручную, но и автоматически сгенерированный. Предупреждать за такой код нам не будет интересно, поэтому мы исключим их из анализа. Здесь на помощь приходят настройки «Не проверять файлы». Специально для этого проекта я указал следующие имена файлов:

pywrap_*
*.pb.cc

Это позволило скрыть более 100 предупреждений общего анализа (ГА) среднего уровня достоверности.

Отключение определенных диагнозов

Еще одна очень полезная настройка анализатора - отключение групп диагностических правил. Почему это может быть актуально? Например, было около 70 предупреждений V730 (не все члены класса инициализируются в конструкторе). Эти предупреждения действительно нуждаются в проверке, поскольку они могут сигнализировать о трудно обнаруживаемых ошибках. Тем не менее, человеку, мало знакомому с кодом, может быть непонятно, приведет ли неинициализированный член к проблемам или есть другой хитрый способ его дальнейшей инициализации. Для статьи эти ошибки тоже не представляют особого интереса. Вот почему разработчикам действительно стоит их просмотреть, и мы не будем на этом останавливаться. Поэтому у нас есть цель - отключить целую группу диагностических правил. Это легко сделать: в настройках плагина PVS-Studio достаточно снять галочку с нужной диагностики.

Рисунок 6. Отключение нерелевантных диагнозов

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

Предупреждения анализатора, выданные для проекта

Что ж, а теперь перейдем к самому интригующему - тем фрагментам кода, которые анализатор посчитал подозрительными.

Обычно мне нравится начинать с классической ошибки, которая возникает как в C #, так и в C ++ проектах - ошибка a == a, которая обнаруживается диагностическими правилами V501 и V3001. Но здесь таких ошибок нет! В общем, ошибки, обнаруженные в этом проекте… весьма своеобразны… Итак, поехали.

void ToGraphDef(const Graph* g, GraphDef* gdef, bool pretty) {
  ....
  gtl::InlinedVector<const Edge*, 4> inputs;
  ....
  for (const Edge* e : inputs) {
    const string srcname = NewName(e->src(), pretty);
    if (e == nullptr) {
      ndef->add_input("unknown");
    } else if (!e->src()->IsOp()) {
    } else if (e->IsControlEdge()) {
      ndef->add_input(strings::StrCat("^", srcname));
    } else if (e->src_output() == 0) {
      ndef->add_input(srcname);
    } else {
      ndef->add_input(strings::StrCat(srcname, ":", e->src_output()));
    }
  }
  ....
}

Ссылка на GitHub.

Предупреждение PVS-Studio: V595 Указатель e использовался до проверки на nullptr. Проверьте строки: 1044, 1045. function.cc 1044

В цикле мы видим, что определенные элементы вектора повторяются и в зависимости от значения элементов выполняются определенные действия. Проверка e == nullptr предполагает, что указатель может быть нулевым. Дело в том, что мы видим разыменование этого указателя при вызове функции NewName: e- ›src (). Результатом такой операции является неопределенное поведение, которое, помимо прочего, может привести к сбою программы.

Но код TensorFlow не так прост. Заполнение этого вектора (входы) происходит раньше и выглядит следующим образом:

for (const Edge* e : n->in_edges()) {
  if (e->IsControlEdge()) {
    inputs.push_back(e);
  } else {
    if (inputs[e->dst_input()] == nullptr) {
      inputs[e->dst_input()] = e;
    } else {
      LOG(WARNING) << "Malformed graph node. multiple input edges: "
                   << n->DebugString();
    }
  }
}

Внимательно взглянув на код, вы можете понять, что нулевые указатели никогда не будут записаны в вектор input, потому что всегда будет разыменование нулевого указателя перед добавлением элементов, кроме того, что проверка на nullptr выполняется отсутствует перед разыменованием указателя. Поскольку вектор input не будет содержать нулевых указателей, оказывается, что выражение e == nullptr,, о котором мы говорили ранее, всегда будет ложным.

Как бы то ни было, код этот очень хитрый, и PVS-Studio успешно его нашла. Давайте двигаться дальше.

Status MasterSession::StartStep(const BuildGraphOptions& opts, 
                                int64* count,
                                ReffedClientGraph** rcg, 
                                bool is_partial) {
  ....
  ReffedClientGraph* to_unref = nullptr;
  ....
  if (to_unref) to_unref->Unref();
  ....
}

Ссылка на GitGub.

Предупреждение PVS-Studio: V547 Выражение to_unref всегда ложно. master_session.cc 1114

В теле метода мы видим, что объявлена ​​локальная переменная to_unref, инициализируемая значением nullptr. Перед оператором if этот указатель никоим образом не используется, его значение не изменяется. Таким образом, тело оператора if не будет выполнено, потому что указатель остался нулевым. Возможно, этот код остался после рефакторинга. Есть вероятность, что этот указатель должен был использоваться где-то между инициализацией и проверкой, но вместо него программист использовал другой (перепутал), но похожих имен я не нашел. Выглядит подозрительно.

Продолжим.

struct LSTMBlockCellBprop ....
{
  ....
  void operator()(...., bool use_peephole, ....) {
  ....
  if (use_peephole) {
    cs_prev_grad.device(d) =
        cs_prev_grad +
        di * wci.reshape(p_shape).broadcast(p_broadcast_shape) +
        df * wcf.reshape(p_shape).broadcast(p_broadcast_shape);
  }
  if (use_peephole) {
    wci_grad.device(d) = 
      (di * cs_prev).sum(Eigen::array<int, 1>({0}));
    wcf_grad.device(d) = 
      (df * cs_prev).sum(Eigen::array<int, 1>({0}));
    wco_grad.device(d) = 
      (do_ * cs).sum(Eigen::array<int, 1>({0}));
  }
  ....
  }
};

Ссылка на GitHub.

Предупреждение PVS-Studio: V581 Условные выражения операторов if, расположенных рядом друг с другом, идентичны. Проверить строки: 277, 284. lstm_ops.h 284

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

Нельзя просто написать и пропустить ошибки копирования-вставки.

struct CompressFlags {
  ....
  Format format;
  ....
  int quality = 95;
  ....
  bool progressive = false;
  ....
  bool optimize_jpeg_size = false;
  ....
  bool chroma_downsampling = true;
  ....
  int density_unit = 1;  
  int x_density = 300;
  int y_density = 300;
  ....
  StringPiece xmp_metadata;
  ....
  int stride = 0;
};
class EncodeJpegOp : public OpKernel {
  ....
  explicit EncodeJpegOp(OpKernelConstruction* context) :  
    OpKernel(context) { 
    ....
    OP_REQUIRES_OK(context, 
      context->GetAttr("quality", &flags_.quality));
    OP_REQUIRES(context, 
      0 <= flags_.quality && flags_.quality <= 100,
      errors::InvalidArgument("quality must be in [0,100], got ",
      flags_.quality));
    OP_REQUIRES_OK(context,
      context->GetAttr("progressive", 
                       &flags_.progressive));
    OP_REQUIRES_OK(context, 
      context->GetAttr("optimize_size", 
                       &flags_.optimize_jpeg_size));
    OP_REQUIRES_OK(context, 
      context->GetAttr("chroma_downsampling",         // <=
                       &flags_.chroma_downsampling));
    OP_REQUIRES_OK(context, 
      context->GetAttr("chroma_downsampling",         // <=
                       &flags_.chroma_downsampling));
    ....
  }
  ....  
  jpeg::CompressFlags flags_;
}

Ссылка на GitHub.

Предупреждение PVS-Studio: V760 Обнаружены два идентичных блока текста. Второй блок начинается со строки 58. encode_jpeg_op.cc 56

Как видно из кода, программист проверяет значения флагов, считываемых из поля flags_ в конструкторе класса EncodeJpegOp через макрос OP_REQUIRES_OK и OP_REQUIRES. Однако в последних строках данного фрагмента значение того же флага проверяется для конструктора. Это очень похоже на копипаст: код копировали, но не редактировали.

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

Изучив тело конструктора, я не нашел проверки поля шаг. Возможно, в одном из случаев должна была состояться именно эта проверка. С другой стороны, порядок полей в конструкторе аналогичен порядку объявления полей в структуре CompressFlags. Таким образом, сложно сказать, как следует исправить этот код, мы можем только делать предположения. В любом случае на этот код стоит обратить внимание.

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

class InferenceContext {
  ....
  inline int64 Value(DimensionOrConstant d) const {
    return d.dim.IsSet() ? d.dim->value_ : d.val;
  }
  ....
}
REGISTER_OP("UnpackPath")
    .Input("path: int32")
    .Input("path_values: float")
    .Output("unpacked_path: float")
    .SetShapeFn([](InferenceContext* c) {
      ....
      int64 num_nodes = InferenceContext::kUnknownDim;
      if (c->ValueKnown(tree_depth)) {
        num_nodes = (1 << c->Value(tree_depth)) - 1;    // <=
      }
      ....
    })
....;

Ссылка на GitHub.

Предупреждение PVS-Studio: V629 Рассмотрите возможность проверки выражения 1 ‹---------------- c-› Value (tree_depth) . Битовый сдвиг 32-битного значения с последующим расширением до 64-битного типа. unpack_path_op.cc 55

Странность этого кода состоит в том, что 32- и 64-битные значения смешиваются в операциях сдвига и присваивания. Литерал 1 - это 32-битное значение, для которого выполняется сдвиг влево. Результат сдвига по-прежнему имеет 32-битный тип, но записывается в 64-битную переменную. Это подозрительно, потому что мы можем получить неопределенное поведение, если значение, возвращаемое методом Value, больше 32.

Вот цитата из стандарта: Значение E1 ‹* E2 - битовые позиции E2 со смещением влево E1; освобожденные биты заполняются нулями. Если E1 имеет беззнаковый тип, значение результата будет E1 * 2 ^ E2, уменьшенное по модулю на единицу больше, чем максимальное значение, представленное в типе результата. В противном случае, если E1 имеет тип со знаком и неотрицательное значение, а E1 * 2 ^ E2 может быть представлен в типе результата, то это результирующее значение; в противном случае поведение не определено.

Этот код можно исправить, записав 1 как 64-битный литерал или выполнив расширение типа посредством приведения. Подробнее о сменных операциях читайте в статье «Не в безвестные воды. Часть третья"".

Расширение через отливку использовалось и в другом фрагменте. Вот код:

AlphaNum::AlphaNum(Hex hex) {
  ....
  uint64 value = hex.value;
  uint64 width = hex.spec;
  // We accomplish minimum width by OR'ing in 0x10000 to the user's  
  // value,
  // where 0x10000 is the smallest hex number that is as wide as the 
  // user
  // asked for.
  uint64 mask = ((static_cast<uint64>(1) << (width - 1) * 4)) | value;
  ....
}

Ссылка на GitHub.

Предупреждение PVS-Studio: V592 Выражение было дважды заключено в круглые скобки: ((выражение)). Одна пара скобок не нужна или имеется опечатка. strcat.cc 43

Этот код действительно правильный, но анализатор посчитал его подозрительным, обнаружив повторяющиеся круглые скобки. Анализатор думает так: двойные скобки не влияют на результат оценки, поэтому, возможно, одна пара скобок стоит не там, где она должна быть.

Мы не можем исключить, что скобки, вероятно, предназначались для того, чтобы явно подчеркнуть приоритет оценок и избежать запоминания приоритетов операций ‘‹*’ и ‘*’. В них нет особой необходимости, так как они все равно не в том месте. Я считаю, что этот порядок оценки правильный (сначала мы указываем значение сдвига, а затем выполняем сам сдвиг), поэтому нам просто нужно поставить скобки в правильном порядке, чтобы они не сбивали людей с толку.

uint64 mask = (static_cast<uint64>(1) << ((width - 1) * 4)) | value;

Продолжим.

void Compute(OpKernelContext* context) override {
  ....
  int64 v = floor(in_x);
  ....
  v = ceil(in_x1);
  x_interp.end = ceil(in_x1);
  v = x_interp.end - 1;
  ....
}

Ссылка на GitHub.

Предупреждение PVS-Studio: V519 переменной v дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 172, 174. resize_area_op.cc 174

Переменная v назначается дважды, но между этими назначениями значение этой переменной никоим образом не используется. Более того, переменной x_interp.end присваивается то же значение переменной, которое было записано в переменную v. Даже если мы опустим тот факт, что вызов функции ceil здесь избыточен, поскольку он не критичен (хотя…), код выглядит странно: либо он написан странно, либо содержит хитрую ошибку .

Что дальше?

void Compute(OpKernelContext* context) override {
  ....
  int64 sparse_input_start;                                     // <=
  ....
  if (sparse_input) {
    num_total_features += GetNumSparseFeatures(
      sparse_input_indices, *it, &sparse_input_start);          // <=
  }
  if (num_total_features == 0) {
    LOG(WARNING) << "num total features is zero.";
    break;
  }
  if (rand_feature < input_spec_.dense_features_size()) {
    ....
  } else {
    ....
    const int32 sparse_index = sparse_input_start +             // <=
      rand_feature - input_spec_.dense_features_size();
    ....
  }
  ....
}

Ссылка на GitHub.

Предупреждение PVS-Studio: V614 Использована потенциально неинициализированная переменная sparse_input_start. sample_inputs_op.cc 351

Подозрительным в этом коде является то, что во время инициализации константы sparse_index также может использоваться потенциально неинициализированная переменная sparse_input_start. Во время объявления эта переменная не инициализирована никаким значением, т.е. она содержит какой-то мусор. Далее, если утверждение sparse_input истинно, адрес переменной sparse_input_start передается в функцию GetNumSparseFeatures, где, возможно, переменная инициализация происходит. В противном случае, если тело этого условного оператора не выполняется, sparse_input_start останется неинициализированным.

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

Это все?

Ну и да, и нет. Если честно, я надеялся найти больше недочетов и написать статью в стиле статей Qt, Mono, Unreal Engine 4 и им подобных, но не вышло. Авторы проекта проделали большую работу, ошибок обнаружено не так много. Я также надеялся, что проект будет большего размера, но в выбранной конфигурации было проверено только 700 файлов, включая автоматически сгенерированные файлы.

Кроме того, многие вещи остались за рамками данной статьи, например:

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

Тем не менее, было обнаружено немало интересных фрагментов, которые мы рассмотрели в этой статье.

Подводя итоги

TensorFlow оказался довольно интересным и качественным с точки зрения кода проектом, но, как мы видели, не без изъянов. В то же время PVS-Studio еще раз доказал, что умеет находить ошибки даже в коде известных разработчиков.

В заключение не могу не похвалить всех разработчиков TensorFlow за качественный код и пожелать им удачи в будущем.

Спасибо за внимание тем, кто дочитал статью до конца, и не забывайте пользоваться PVS-Studio!