برنامه نویسی

رفع اشکالات در هوش مصنوعی: بیایید اشکالات را در OpenVINO تجزیه و تحلیل کنیم

نویسنده: الکسی گورشکوف

توسعه دهندگان همکار، از شما دعوت می کنیم به سفر هیجان انگیز ما در اعماق کد OpenVINO اینتل ادامه دهید! مجهز به یک تحلیلگر استاتیک، درست مانند کارآگاهان، موذیانه ترین و جالب ترین باگ ها را کشف خواهیم کرد و اسرار خطاهای پنهان و اشتباهات تایپی حیله گر را که خارج از قسمت اول مقاله باقی مانده است، آشکار خواهیم کرد.

image1

معرفی

بنابراین، در قسمت اول به بسیاری از اشتباهات تایپی روشنگر در کد پروژه OpenVINO نگاه کردیم. آنها روشنگر هستند زیرا اشتباهات تایپی همیشه در زندگی یک توسعه دهنده اتفاق می افتد – شما نمی توانید از آنها فرار کنید – اما مشکل قابل حل است. با استفاده از فناوری تجزیه و تحلیل استاتیک که به ما در مورد چنین خطاهایی هشدار می دهد، می توانیم آمار خاصی در مورد مکان هایی که برنامه نویس اغلب این اشتباهات را انجام می دهد ایجاد کنیم. هنگامی که این الگوها شناسایی شوند، مبارزه با اشتباهات تایپی بسیار مؤثرتر می شود. و اگر فکر می کنید رایج ترین اشتباهات تایپی کجاست، مقاله جالبی داریم که ارزش خواندن دارد.

با این حال، تصور کنید اگر در یک چشم به هم زدن می‌توانستیم کاملاً روی کد تمرکز کنیم بدون اینکه حواس‌مان پرت شود. سپس کد ما کامل خواهد بود و اشتباهات تایپی کاملاً ناپدید می شوند. بیایید به رویاپردازی ادامه دهیم… اگر فقط می‌توانستیم تمام سناریوهای ممکن استفاده از کد را پیش‌بینی کنیم، همه اتصالات بین اجزای آن را در نظر بگیریم و هر خطا را پیش‌بینی کنیم… نرم‌افزاری که ما توسعه داده‌ایم کاملاً کار می‌کرد. با این حال، من فقط یک “زبان برنامه نویسی” را می دانم که در آن همه اینها امکان پذیر است و آن HTML است.

با این حال، ما هنوز در مورد C++ صحبت می کنیم. هنگام برخورد با آن، حتی با تجربه ترین برنامه نویس هم می تواند اشتباه کند، هوم… خوب، واقعاً، در هر کجا و همه جا. بنابراین، اکنون که همه اخطارها را به «قبل» و «بعد» تقسیم کرده‌ایم و به اشتباهات املایی پرداخته‌ایم، بیایید به اشتباهات دیگر، نه کمتر جالب و متنوع، نگاهی بیندازیم.

هدف مقاله بی ارزش کردن کار برنامه نویسان درگیر در توسعه این محصول نیست. هدف آن رایج کردن تحلیلگرهای کد ایستا است که حتی برای پروژه های با کیفیت بالا و تاسیس شده مفید هستند. علاوه بر این، ما مجوزهای رایگان برای پروژه های منبع باز (و نه فقط برای آن ها) ارائه می دهیم. شما می توانید اینجا بیشتر بیاموزید.

نتایج را بررسی کنید

در واقع، تحلیلگر آنقدر خطا پیدا نکرد. تمام خطاهای جالب و مهم فقط در چند مقاله جای می گیرند. کد بسیار زیبا و امن است. commitی که من برای ساخت و آزمایش پروژه استفاده می کردم هنوز یکسان است: 2d8ac08.

خب، حالا ما تشریفات را تمام کردیم، پس بیایید به بررسی خطاها برگردیم. همانطور که انتظار می رفت، پیچیدگی باگ افزایش می یابد و پیچیدگی کدی که باید کشف کنیم نیز افزایش می یابد. برخی از قسمت های کد بسیار عجیب هستند. معلوم نیست چطور و چرا اینطور نوشته شده، اما پیشنهاد می کنم از چند چیز ساده شروع کنید و بعد… خب، به زودی خواهید دید.

از ساده به پیچیده. کد کاملا واضح نیست

قطعه N1

template typename T>
void ROIAlignForward(....) 
{
  int64_t roi_cols = 4;       // 

  int64_t n_rois = nthreads / channels / pooled_width / pooled_height;
  // (n, c, ph, pw) is an element in the pooled output
  for (int64_t n = 0; n  n_rois; ++n) 
  {
    ....
    if (roi_cols == 5)        // 
    {
      roi_batch_ind = static_castint64_t>(offset_bottom_rois[0]);
      offset_bottom_rois++;
    }
    ....
  }
....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار تحلیلگر: V547 عبارت 'roi_cols == 5' همیشه نادرست است. Experimental_detectron_roi_feature_extractor.cpp 211

چک کردن اگر (roi_cols == 5) واقعا همیشه برمیگرده نادرستو کد موجود در بدنه غیرقابل دسترسی است. این به این دلیل است که ارزش roi_cols متغیر به هیچ وجه بین زمانی که اعلام می شود و زمانی که در شرایط بررسی می شود تغییر نمی کند.

قطعه N2

bool is_valid_model(std::istream& model) 
{
  // the model usually starts with a 0x08 byte
  // indicating the ir_version value
  // so this checker expects at least 3 valid ONNX keys
  // to be found in the validated model
  const size_t EXPECTED_FIELDS_FOUND = 3u;
  std::unordered_set<::>onnx::Field, std::hashint>> onnx_fields_found = {};
  try 
  {
    while (!model.eof() && onnx_fields_found.size()     // 
           EXPECTED_FIELDS_FOUND                      ) 
    {
      const auto field = ::onnx::decode_next_field(model);

      if (onnx_fields_found.count(field.first) > 0) 
      {
        // if the same field is found twice, this is not a valid ONNX model
        return false;
      }
      else
      {
        onnx_fields_found.insert(field.first);
        ::onnx::skip_payload(model, field.second);
      }
    }

      return onnx_fields_found.size() == EXPECTED_FIELDS_FOUND;
  }
  catch (...)
  {
    return false;
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالیزور: V663 Loop Infinite امکان پذیر است. شرط 'cin.eof()' برای شکستن از حلقه کافی نیست. فراخوانی تابع 'cin.fail()' را به عبارت شرطی در نظر بگیرید. onnx_model_validator.cpp 168

تحلیلگر یک اشکال نسبتاً جالب و نادر را شناسایی کرد که می تواند باعث بی نهایت شدن یک حلقه شود.

هنگام کار با اشیاء از std:: جریان کلاس، !model.eof() بررسی ممکن است برای خروج از a کافی نباشد در حالی که حلقه اگر هنگام خواندن داده ها خرابی رخ دهد، همه تماس های بعدی به eof فقط بازگشت تابع نادرست، که ممکن است منجر به یک حلقه بی نهایت شود.

برای جلوگیری از این مشکل، می توانیم از اپراتور اضافه بار استفاده کنیم بوول در شرایط حلقه به شرح زیر است:

while (model && onnx_fields_found.size()  EXPECTED_FIELDS_FOUND) 
{
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه N3

NamedOutputs pad3d(const NodeContext& node) 
{
  ....
  // padding of type int feature only supported by paddle 'develop'
  // version(>=2.1.0)
  if (node.has_attribute("paddings"))                            // 
  {
    auto paddings_vector = node.get_attribute
                             std::vectorint32_t>
                           >("paddings");
    PADDLE_OP_CHECK(node, paddings_vector.size() == 6, 
                    "paddings Params size should be 6 in pad3d!");
    paddings = paddings_vector;

  } 
  else if (node.has_attribute("paddings"))                       // 
  {
    auto padding_int = node.get_attributeint32_t>("paddings");
    for (int i = 0; i  6; i++)
      paddings[i] = padding_int;
  } 
  else 
  {
    PADDLE_OP_CHECK(node, false, "Unsupported paddings attribute!");
  }
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالایزر: V517 [CERT-MSC01-C] استفاده از الگوی 'if (A) {…} else if (A) {…}' شناسایی شد. احتمال وجود خطای منطقی وجود دارد. بررسی خطوط: 22، 26. pad3d.cpp 22

شرایط اول و دوم از اگر ساختارها یکسان هستند، بنابراین کد موجود در سپس-شاخه دوم اگر همیشه دست نیافتنی است ممکن است متوجه شوید که شاخه‌ها منطق متفاوتی دارند: اولی سعی می‌کند منطق را بخواند بالشتک ها ویژگی به عنوان بردار از int32_t (paddings_vector) نوع عدد، در حالی که دومی سعی می کند همان ویژگی را به عنوان یک عدد بخواند نوع int32_t (padding_int).

تعیین اینکه کد صحیح دقیقاً در این مورد چگونه باید باشد دشوار است. با این حال، بیایید یک حدس بزنیم. کد در ماژول OpenVINO Paddle Frontend است که مدل تولید شده توسط چارچوب PaddlePaddle را تجزیه می کند. اگر نام “pad3d” را در پروژه جستجو کنیم، می توانیم توضیحات زیر را پیدا کنیم:

Parameters:
  padding (Tensor|list[int]|tuple[int]|int): The padding size with data type
            ``'int'``. If is ``'int'``, use the same padding in all dimensions.
            Else [len(padding)/2] dimensions of input will be padded.
            The pad has the form (pad_left, pad_right, pad_top,
                                  pad_bottom, pad_front, pad_back).
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این نشان می دهد که لایه گذاری یک گزینه است، و ما باید از دو جایگزین جالب دیدن کنیم: std:: vector و int32_t. ما می توانیم این کار را به روش زیر انجام دهیم:

auto paddings = std::vectorint32_t>(6, 0);

if (node.has_attribute("paddings"))
{
  auto paddings_attr = node.get_attribute_as_any("paddings");
  if (paddings_attr.isstd::vectorint32_t>>())
  {
    auto paddings_vector = paddings_attr.asstd::vectorint32_t>>();
    PADDLE_OP_CHECK(node, paddings_vector.size() == 6,
                    "paddings Params size should be 6 in pad3d!");
    paddings = std::move(paddings_vector);
  }
  else if (paddings_attr.isint32_t>())
  {
    auto padding_int = paddings_attr.asint32_t>();
    if (padding_int != 0)
    {
      std::fill(paddings.begin(), paddings.end(), padding_int);
    }
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همچنین، در حالی که به منابع PaddlePaddle نگاه می‌کردم، متوجه شدم که یک اشتباه تایپی در ویژگی وجود دارد. بنابراین، باید نامیده شود لایه گذاری، نه بالشتک ها. اما کاملا مطمئن نیستم 🙂 در هر صورت به توسعه دهندگان توصیه می کنم به این کد توجه کنند.

قطعه N4

template typename T>
std::basic_stringT> get_model_extension() {}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالایزر: V591 [CERT-MSC52-CPP] تابع non-void باید مقداری را برگرداند. graph_iterator_flatbuffer.hpp 29

همانطور که می بینیم، تابع دارای بدنه خالی است و هیچ چیز را برمی گرداند، حتی اگر نوع بازگشتی به صورت مشخص شده باشد std::basic_string. خب سلام رفتار تعریف نشده

این قطعه کد به نظر می رسد که مستقیماً از سری X-Files آمده است، اما در واقع بسیار ساده است. اگر چند خط به پایین پرش کنیم، می‌توانیم تخصص‌های این الگوی تابع را ببینیم:

template >
std::basic_stringchar> get_model_extensionchar>();

#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) \
 && defined(_WIN32)

template >
std::basic_stringwchar_t> get_model_extensionwchar_t>();

#endif
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و این تخصص ها دارای بدنه های خاصی هستند که اشیاء خاصی را برمی گرداند:

template >
std::basic_stringchar>
ov::frontend::tensorflow_lite::get_model_extensionchar>()
{
  return ::tflite::ModelExtension();
}

#if defined(OPENVINO_ENABLE_UNICODE_PATH_SUPPORT) \
 && defined(_WIN32)

template >
std::basic_stringwchar_t> ov::frontend::
                            tensorflow_lite::get_model_extensionwchar_t>()
{
  return util::string_to_wstring(::tflite::ModelExtension());
}

#endif
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین، این برنامه به درستی با تخصص های از کاراکتر و wchar_t اما هر کاری که بخواهد با دیگران انجام می دهد. و سه مورد دیگر وجود دارد: char8_t، char16_t، char32_t.

بله، ممکن است کد از این تخصص های قالب استفاده نکند، اما همه ما برای کد ایمن و مطمئن هستیم. و ما دوست داریم که کامپایلر ما را در مرحله کامپایل زمانی که با چنین کدی سروکار داریم متوقف کند. انجام این کار بسیار آسان است، فقط باید تعریف قالب تابع را به یک اعلان تبدیل کنیم:

template typename T>
std::basic_stringT> get_model_extension();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حال حاضر، زمانی که ما سعی می کنیم به یک تخصص از char8_t، char16_t، یا char32_t، کامپایلر خطا می دهد زیرا نمی تواند نمونه سازی مورد نیاز را بدون بدنه انجام دهد.

در اینجا چند هشدار دیگر وجود دارد:

  • V591 [CERT-MSC52-CPP] تابع non-void باید مقداری را برگرداند. graph_iterator_meta.hpp 18
  • V591 [CERT-MSC52-CPP] تابع non-void باید مقداری را برگرداند. graph_iterator_saved_model.hpp 19
  • V591 [CERT-MSC52-CPP] تابع non-void باید مقداری را برگرداند. graph_iterator_saved_model.hpp 21

قطعه N5

template typename TReg>
int getFree(int requestedIdx)
{
  if (std::is_base_ofXbyak::Mmx, TReg>::value)
  {
    auto idx = simdSet.getUnused(requestedIdx);
    simdSet.setAsUsed(idx);
    return idx;
  }
  else if (   std::is_sameTReg, Xbyak::Reg8>::value
           || std::is_sameTReg, Xbyak::Reg16>::value
           || std::is_sameTReg, Xbyak::Reg32>::value
           || std::is_sameTReg, Xbyak::Reg64>::value)
  {
    auto idx = generalSet.getUnused(requestedIdx);
    generalSet.setAsUsed(idx);
    return idx;
  }
  else if (std::is_sameTReg, Xbyak::Opmask>::value)
  {
    return getFreeOpmask(requestedIdx);
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالایزر: V591 [CERT-MSC52-CPP] تابع non-void باید مقداری را برگرداند. registers_pool.hpp 229

در اینجا یک قطعه دیگر با یک تابع است که مقداری را در همه مسیرهای اجرا بر نمی گرداند. اگر فکر می کنید در صورت فراموشی هیچ اتفاقی برای برنامه شما نمی افتد برگشت در یک تابع، شما اشتباه می کنید. حتی اگر نوع بازگشتی داخلی باشد، کد حاوی رفتار تعریف نشده است. همچنین خواندن این مقاله در مورد شوخی بی رحمانه ای که کامپایلر می تواند در این شرایط با شما انجام دهد را توصیه می کنم.

بیایید به مثال خود برگردیم. با شروع با C++17، می‌توانیم این کد را تقویت کنیم. در مرحله اول، همانطور که می بینید، شرایط عبارت های زمان کامپایل هستند. بنابراین، بیایید از آن استفاده کنیم اگر constexpr ساختن. کامپایل کردن کد را در شاخه هایی که شرط وجود دارد کنار می گذارد نادرست. ثانیاً می توانیم استفاده کنیم static_adsert برای محافظت از کد در برابر تخصص هایی که هیچ مقداری در کد فعلی برای آنها برگردانده نشده است:

template typename TReg>
int getFree(int requestedIdx)
{
  if constexpr (std::is_base_ofXbyak::Mmx, TReg>::value) { .... }
  ....
  else
  {
    // until C++23
    static_assert(sizeof(TReg *) == 0, "Your message.");

    // since C++23
    static_assert(false, "Your message."); 
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مثال ثابت دو راه برای نوشتن ارائه می دهد static_adsertبسته به نسخه استانداردی که استفاده می کنید. این به این دلیل است که قبل از C++23، static_assert(false، “…”) گزینه در قالب تابع همیشه منجر به خطای زمان کامپایل می شود.

اگر با نسخه‌های قبل از C++17 کار می‌کنید، می‌توانید کد را با اضافه کردن بارهای اضافه به آن برطرف کنید دریافت رایگان الگوی تابع و استفاده از std::enable_if:

template typename TReg,
          std::enable_if_t std::is_base_ofXbyak::Mmx, TReg>::value,
                            int > = 0>
int getFree(int requestedIdx)
{
  auto idx = simdSet.getUnused(requestedIdx);
  simdSet.setAsUsed(idx);
  return idx;
}

template typename TReg,
          std::enable_if_t std::is_sameTReg, Xbyak::Reg8>::value
                         || std::is_sameTReg, Xbyak::Reg16>::value
                         || std::is_sameTReg, Xbyak::Reg32>::value
                         || std::is_sameTReg, Xbyak::Reg64>::value,
                            int > = 0>
int getFree(int requestedIdx)
{
  auto idx = generalSet.getUnused(requestedIdx);
  generalSet.setAsUsed(idx);
  return idx;
}

template typename TReg,
          std::enable_if_t std::is_sameTReg, Xbyak::Opmask>::value,
                            int > = 0>
int getFree(int requestedIdx)
{
  return getFreeOpmask(requestedIdx);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه N6

template >
void RandomUniformx64::avx512_core>::initVectors()
{
  ....
  if (m_jcp.out_data_type.size()  4)
  {
    static const uint64_t n_inc_arr[8]  = { 0, 1, 2, 3, 4, 5, 6, 7 };
    mov(r64_aux, reinterpret_castuintptr_t>(n_inc_arr));
  }
  else
  {
    static const uint64_t n_inc_arr[8]  = 
                                    { 0, 1, 2, 3, 4, 5, 6, 7 }; // TODO: i64
    mov(r64_aux, reinterpret_castuintptr_t>(n_inc_arr));
  }
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار تحلیلگر: V523 عبارت «then» معادل عبارت «else» است. random_uniform.cpp 120

این قطعه ساده اما نامشخص است. همان کد در بدنه هر دو موجود است اگر و دیگر شاخه ها. شاید این یک خطای کپی پیست باشد و توسعه دهندگان باید به این قطعه کد توجه کنند.

قطعه N7

inline ParseResult parse_xml(const char* file_path) 
{
  ....
  try
  {
    auto xml = std::unique_ptrpugi::
                          xml_document>{new pugi::xml_document{}};
    const auto error_msg = [&]() -> std::string {....}();
    ....

    return {std::move(xml), error_msg};
  }
  catch (std::exception& e) 
  {
    return {std::move(nullptr),               // 
            std::string("Error loading XML file: ") + e.what()};
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالایزر: V575 [CERT-EXP37-C] اشاره گر تهی به تابع “حرکت” منتقل می شود. آرگومان اول را بررسی کنید. xml_parse_utils.hpp 249

صادقانه بگویم، ما قبلاً چنین قطعه کد جالبی را ندیده بودیم: nullptr گذشت به std::حرکت تابع. عبور ممنوع نیست nullptr به std::حرکت، تابع فقط آن را به تبدیل می کند std::nullptr_t &&. با این حال، مشخص نیست که چرا این کار انجام شده است.

برای درک بهتر ماجرا، بیایید نگاهی به آن بیندازیم ParseResult:

struct ParseResult 
{
  ParseResult(std::unique_ptrpugi::xml_document>&& xml, std::string error_msg)
        : xml(std::move(xml)),
          error_msg(std::move(error_msg)) {}

  std::unique_ptrpugi::xml_document> xml;

  std::string error_msg{};
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید کارآگاه بازی کنیم و معمای این که چنین کدی از کجا آمده است را حل کنیم. به احتمال زیاد یک برنامه نویس نوشته است برگشت در تلاش كردن-block first: ParseResult شی در آنجا با حرکت دادن ساخته می شود xml اشاره گر هوشمند و کپی کردن error_msg. سپس آن‌ها شرایطی را که در آن استثناء ایجاد می‌شد، کنترل کردند. در این مورد، یک شی از ParseResult نوع نیز باید برگردانده شود. آنها برای اینکه زندگی خود را آسان تر کنند، قبلی را کپی کردند برگشت و آرگومان های سازنده را کمی تغییر داد. وقتی دیدند xml با حرکت اشاره گر هوشمند، آنها تصمیم گرفتند که باید چیزی را به اینجا نیز منتقل کنند. را nullptr برای مثال اشاره گر

با این حال، نیازی به آن نیست. با توجه به قوانین ++C، زمانی که اضافه بار یک سازنده را انتخاب می کنید، کامپایلر باید برخی از تبدیل های ضمنی را روی آرگومان های ارسال شده انجام دهد. به عنوان مثال rvalue ارجاع به std::unique_ptr<:xml_document/> نمی توان به rvalue ارجاع به std::nullptr_t. بنابراین، کامپایلر یک شی موقت از the ایجاد می کند std::unique_ptr<:xml_document/> با فراخوانی سازنده تبدیل مناسب تایپ کنید. تنها پس از آن یک مرجع (پارامتر سازنده) به این شی موقت محدود می شود.

اگر حذف کنیم std::حرکت و عبور کنید nullptr برای سازنده، کد نیز کامپایل می شود و خواناتر می شود:

return { nullptr, std::string("Error loading XML file: ") +
         e.what() };
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اشاره گرهای تهی و نشت احتمالی حافظه

و در اینجا به طور یکپارچه به کار با اشاره گرها می پردازیم. و تجزیه و تحلیل کمک می کند تا مواردی که در آن چیزها اشتباه پیش رفتند و اینکه چگونه نباید به طور کلی کدنویسی کرد.

قطعه N8

void GraphOptimizer::FuseFCAndWeightsDecompression(Graph &graph) 
{
  ....

  // Fusion processing
  auto *multiplyInputNode = dynamic_castnode::
                                         Input *>(multiplyConstNode.get());
  if (!multiplyInputNode)
  {
    OPENVINO_THROW("Cannot cast ", multiplyInputNode->getName(),   // 
                   " to Input node.");
  }
  fcNode->fuseDecompressionMultiply(multiplyInputNode->getMemoryPtr());

  if (withSubtract)
  {
    auto *subtractInputNode = dynamic_castnode::
                                           Input *>(subtractConstNode.get());
    if (!subtractInputNode)
    {
      OPENVINO_THROW("Cannot cast ", subtractInputNode->getName(), // 
                     " to Input node.");
    }
    fcNode->fuseDecompressionSubtract(subtractInputNode->getMemoryPtr());
  }
  if (withPowerStatic)
  {
    auto *eltwiseNode = dynamic_castnode::
                                     Eltwise *>(powerStaticNode.get());
    if (!eltwiseNode) 
    {
      OPENVINO_THROW("Cannot cast ", eltwiseNode->getName(),       // 
                     " to Eltwise node.");
    }
  }
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار تحلیلگر:

  • V522 ممکن است ارجاع مجدد نشانگر تهی 'multiplyInputNode' انجام شود. graph_optimizer.cpp 452
  • V522 ممکن است ارجاع مجدد نشانگر تهی 'subtractInputNode' انجام شود. graph_optimizer.cpp 459
  • V522 عدم ارجاع اشاره گر تهی 'eltwiseNode' ممکن است انجام شود. graph_optimizer.cpp 466

من اغلب چنین خطاهایی را در پروژه های مختلف می بینم، بنابراین تصمیم گرفتم به آنها توجه کنم. مسئله این است که برنامه نویسان به ندرت کنترل کننده های خطا را آزمایش می کنند 🙂

بیایید یک لحظه تصور کنیم که dynamic_cast نتیجه یک اشاره گر تهی برمی گرداند. سپس، هنگامی که یک استثنا پرتاب می شود، از همان اشاره گر تهی برای فراخوانی استفاده می شود getName تابع. ما رفتار تعریف نشده ای دریافت می کنیم که می تواند باعث شود مدیریت استثنا به یک خطای بحرانی برای برنامه تبدیل شود.

قطعه N9

void Defer(Task task) 
{
  auto &stream = *(_streams.local());      // 
  stream._taskQueue.push(std::move(task));
  if (!stream._execute) 
  {
    stream._execute = true;
    try
    {
      while (!stream._taskQueue.empty())
      {
        Execute(stream._taskQueue.front(), stream);
        stream._taskQueue.pop();
      }
    }
    catch (...)
    {
    }

    stream._execute = false;
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار تحلیلگر: V758 وقتی اشاره گر هوشمندی که توسط یک تابع برگردانده می شود، مرجع «جریان» نامعتبر می شود. cpu_streams_executor.cpp 410

در واقع، این مثال خوب است و مرجع “آویزان” نیست. این اتفاق می افتد زیرا shared_ptr که محلی تابع برمی گرداند از قبل در ذخیره شده است _stream_map ظرفی که طول عمر بیشتری نسبت به مرجع دارد:

class CustomThreadLocal : public ThreadLocalstd::shared_ptrStream>>
{
  ....
public:
  std::shared_ptrStream> local()
  {
    ....
    if (stream == nullptr)
    {
      stream = std::make_sharedImpl::Stream>(_impl);
    }

    auto tracker_ptr = std::make_shared
                         CustomThreadLocal::ThreadTracker
                       >(id);
    t_stream_count_map[(void*)this] = tracker_ptr;
    auto new_tracker_ptr = tracker_ptr->fetch();
    _stream_map[new_tracker_ptr] = stream;                // 
    return stream;
  }
private:
  ....
  std::mapstd::shared_ptrCustomThreadLocal::ThreadTracker>,
           std::shared_ptrImpl::Stream>> _stream_map;
  ....
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با این حال، در دفاع از آنالیزور، بهتر است نتیجه را ذخیره کنید محلی تابع در یک متغیر در به تعویق انداختن تابع. بیایید تصور کنیم که کد کمی تغییر کرده است. موارد زیر شروع به رخ دادن می کنند:

  1. را محلی تابع اکنون برمی گردد shared_ptr با تعداد مراجع 1.
  2. را به تعویق انداختن کد تابع تغییر نکرده است
  3. را جریان ارزیابی اظهارنامه آغاز می شود. را محلی تابع فراخوانی می شود، یک موقت برمی گرداند shared_ptr با تعداد مرجع 1. The جریان سپس مرجع به شیء زیر اشاره گر هوشمند متصل می شود.
  4. هنگامی که اعلان به طور کامل ارزیابی شد، مخرب اشاره گر هوشمند موقت فراخوانی می شود. تعداد مرجع 0 می شود و شیء زیر اشاره گر هوشمند از بین می رود.
  5. را جریان مرجع آویزان می شود. استفاده از آن منجر به رفتار نامشخص می شود.

با ذخیره نتیجه در متغیر، طول عمر شی را قبل از اینکه از محدوده خارج شود افزایش می دهیم. بنابراین، ما یک مشکل بالقوه را حذف می کنیم:

void Defer(Task task) 
{
  auto local = _streams.local();
  auto &stream = *local;
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه N10

~DIR()
{
  if (!next)
    delete next;
  next = nullptr;
  FindClose(hFind);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار آنالایزر: V575 [CERT-EXP37-C] اشاره گر تهی به “حذف اپراتور” منتقل می شود. استدلال را بررسی کنید. w_dirent.h 94

وقتی به این کد نگاه می کنید، ممکن است در ابتدا نتوانید متوجه شوید که چه خطایی دارد. مسئله این است که حافظه در بعد نشانگر آزاد نمی شود زیرا چک اشتباه نوشته شده است. تحلیلگر به طور غیرمستقیم وقتی می بیند که یک اشاره گر تهی به آن ارسال می شود، آن را مطلع می کند حذف اپراتور. همچنین، هیچ فایده ای برای بررسی نشانگر وجود ندارد زیرا اپراتور حذف نشانگرهای پوچ را به درستی مدیریت می کند.

در اینجا یک مثال کد وجود دارد که تقریباً دقیقاً مشابه همان چیزی است که همکارم در مقاله ارائه کرده است: “خطاهای ساده و در عین حال آسان برای از دست دادن در کد”. توصیه می کنم به آن نگاهی بیندازید. اگر قبلا مقاله را خوانده اید و فکر می کنید که کد داده شده در آنجا مصنوعی است و نمی تواند در زندگی واقعی اتفاق بیفتد، در اینجا یک مدرک مستقیم برای شما وجود دارد 🙂

کد ثابت:

~DIR()
{
  delete next;
  next = nullptr;
  FindClose(hFind);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

قطعه N11

void ov_available_devices_free(ov_available_devices_t* devices) 
{
  if (!devices) 
  {
    return;
  }
  for (size_t i = 0; i  devices->size; i++) 
  {
    if (devices->devices[i]) 
    {
      delete[] devices->devices[i];
    }
  }
  if (devices->devices)
    delete[] devices->devices;
  devices->devices = nullptr;
  devices->size = 0;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هشدار تحلیلگر: V595 نشانگر 'devices->devices' قبل از تأیید در برابر nullptr استفاده شد. بررسی خطوط: 271، 274. ov_core.cpp 271

اول از همه، بیایید ببینیم که چیست ov_available_devices_t نوع این است:

typedef struct {
    char** devices;  //!
    size_t size;     //!
} ov_available_devices_t;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که از نام می بینید، تابع یک شی از the را آزاد می کند ov_available_devices_t نوع به آن منتقل شد. به جز اینکه توسعه دهنده در انجام این کار یک اشتباه مرتکب شده است.

ابتدا، هر اشاره گر در دستگاه ها -> دستگاه ها آرایه در حلقه آزاد می شود. به طور ضمنی اشاره‌گر به آرایه همیشه غیر تهی است. سپس توسعه دهنده به این موضوع شک کرد و پس از حلقه، تصمیم گرفت آن را قبل از ارسال به اپراتور آزمایش کند حذف[]. با این تفاوت که او این فرض را در حلقه فراموش کرده است. در نتیجه، ما یک اشاره گر بالقوه تهی را از ارجاع خارج می کنیم.

این کد ثابت است:

void ov_available_devices_free(ov_available_devices_t* devices) 
{
  if (!devices || !devices->devices)
  {
    return;
  }

  for (size_t i = 0; i  devices->size; i++) 
  {
    delete[] devices->devices[i];
  }

  delete[] devices->devices;
  devices->devices = nullptr;
  devices->size = 0;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که ممکن است متوجه شوید، من تمام چک های اشاره گر را قبل از ارسال آنها به اپراتور حذف کرده ام حذف[]، زیرا می داند چگونه با آنها رفتار کند.

آنالایزر چندین قطعه مشابه دیگر را نیز شناسایی کرد:

  • V595 نشانگر 'versions-> versions' قبل از تأیید در برابر nullptr استفاده شد. بررسی خطوط: 339, 342. ov_core.cpp 339
  • V595 نشانگر 'profiling_infos->profiling_infos' قبل از تأیید در برابر nullptr استفاده شد. بررسی خطوط: 354، 356. ov_infer_request.cpp 354

قطعه N12

char* str_to_char_array(const std::string& str) 
{
  std::unique_ptrchar> _char_array(new char[str.length() + 1]); // 
  char* char_array = _char_array.release();                      // 
  std::copy_n(str.c_str(), str.length() + 1, char_array);
  return char_array;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که در ابتدا وعده داده شده بود – به عنوان گیلاس در بالا – در اینجا کدی وجود دارد که ممکن است باعث شود داور بپرسد “تو چی هستی؟”

ما یک تابع مبدل داریم که کپی می کند std::string به تخصیص پویا کاراکتر* بافر بافر با استفاده از عملگر ایجاد می شود جدید[]، که مالکیت آن به اشاره گر هوشمند منتقل می شود. به طور کلی، بستن یک اشاره گر خام یک استراتژی بسیار خوب است، زیرا اگر مشکلی پیش بیاید، یک اشاره گر هوشمند همه چیز را به عهده می گیرد.

با این حال توجه داشته باشید که std::unique_ptr تخصصی از اشاره گر هوشمند استفاده می شود. ویرانگر آن منبع ارسال شده را با استفاده از عملگر* delete* آزاد می کند. در واقع، این چیزی است که تحلیلگر به ما هشدار می دهد:

V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_core.cpp 14

کار درست در چنین مواردی استفاده از std::unique_ptr تخصص:

char* str_to_char_array(const std::string& str) 
{
  std::unique_ptrchar[]> _char_array(new char[str.length() + 1]);
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

PS خواننده ممکن است اعتراض کند که هیچ مشکلی در اینجا وجود ندارد، زیرا خط بعدی مالکیت منبع را به شی برگشتی می دهد. در واقع، این کار را می کند. با این حال، هنوز یک «بوی کد» است، و مسئله این است که توسعه دهندگان دیگر ممکن است بخواهند از این اعلامیه اشاره گر هوشمند در جای دیگری استفاده کنند.

متأسفانه، روند ضرب از قبل آغاز شده بود:

  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_node.cpp 69
  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_partial_shape.cpp 25
  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_partial_shape.cpp 53
  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_partial_shape.cpp 70
  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_partial_shape.cpp 125
  • V554 استفاده نادرست از unique_ptr. حافظه اختصاص داده شده با 'جدید []” با استفاده از “حذف” پاک خواهد شد. ov_shape.cpp 23

اگر یک عملیات پرتاب استثنا بین اکتساب منبع و انتقال آن به مالکیت اشاره گر خام رخ دهد، رفتار نامشخصی دریافت می کنیم.

PPS اگر پروژه از استاندارد C++14 به بعد استفاده می کند، می توانید کد را با کد زیر جایگزین کنید:

char* str_to_char_array(const std::string& str) 
{
  auto _char_array = std::make_uniquechar[]>(str.length() + 1);
  char* char_array = _char_array.release();
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مرحله اول، استفاده صریح از اپراتور را حذف می کنیم جدید[] در کد، همه چیز را به std::make_unique. ثانیاً خودکار تخصص صحیح اشاره گر هوشمند را بر اساس اولیه ساز استنباط می کند.

با این حال، چنین کدی علاوه بر تخصیص پویا، آرایه را با صفر نیز پر می کند. از آنجایی که آرایه کاملاً بازنویسی شده است، می‌توانیم منابع را با مقداردهی اولیه نکردن آن ذخیره کنیم. از آنجایی که C++20، std::make_unique_for_overwrite تابع برای این منظور در دسترس است:

char* str_to_char_array(const std::string& str) 
{
  auto _char_array = std::make_unique_for_overwritechar[]>(
                       str.length() + 1
                     );
  char* char_array = _char_array.release();
  ....
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه

برخی از نمونه های پروژه OpenVINO واقعاً مرا شگفت زده کرد. من فکر می کنم آنها شما را به همان اندازه شگفت زده کردند. با این حال، همانطور که قبلاً نوشتم، تمام خطاهای مهمی که توسعه دهندگان پروژه باید به آنها توجه می کردند در چند مقاله جمع آوری شده اند. اکثر آنها اشتباهات املایی هستند، که یک مشکل رایج در بین برنامه نویسان است که تقریباً در هر پروژه ای ممکن است رخ دهد. OpenVINO یک پروژه چشمگیر است و داشتن خطاهای بسیار کم (به نظر من) تنها به این معنی است که کد کاملاً خوب نوشته شده است.

امیدوارم شما هم مثل من علاقه مند بوده باشید که به این قطعات کد نگاه کنید و خطاهای موجود در آنها را بررسی کنید.

البته، ما تمام اطلاعات را برای توسعه دهندگان ارسال کرده ایم و امیدواریم در آینده نزدیک آنها باگ ها را برطرف کنند.

و به عنوان یک سنت، من به شما توصیه می کنم که آنالایزر PVS-Studio ما را امتحان کنید. ما مجوز رایگان برای پروژه های منبع باز ارائه می دهیم.

مراقب باشید و روز خوبی داشته باشید!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا