كيف يخفي رمز غريب الأخطاء؟ تحليل TensorFlow.NET

TensorFlow.NET و PVS-Studio

يعد التحليل الثابت أداة مفيدة للغاية لأي مطور ، حيث أنه يساعد في العثور على الوقت ليس فقط الأخطاء ، ولكن أيضًا أجزاء مشبوهة وغريبة من التعليمات البرمجية التي يمكن أن تسبب حيرة للمبرمجين الذين قد يضطرون للعمل معه في المستقبل. سيتم توضيح هذه الفكرة من خلال تحليل لمشروع TensorFlow.NET open C # المفتوح ، والذي يتم تطويره للعمل مع مكتبة TensorFlow الشهيرة للتعلم الآلي.

اسمي نيكيتا ليبيلين. منذ فترة انضممت إلى قسم المبرمجين C # في PVS-Studio. تقليديا ، يكتب جميع الوافدين الجدد إلى الفريق مقالات تناقش نتائج فحص مختلف المشاريع المفتوحة باستخدام محلل ثابت PVS-Studio. تساعد هذه المقالات الموظفين الجدد على التعرف على المنتج بشكل أفضل ، وفي نفس الوقت توفر مزايا إضافية من حيث تعميم منهجية التحليل الثابت. أقترح أن تتعرف على أول عمل لي حول موضوع تحليل المشاريع المفتوحة.

المقدمة


مجموعة متنوعة من الأخطاء المحتملة في رمز البرنامج مذهلة. يكشف بعضهم عن أنفسهم على الفور ، بعد فحص سطحي للتطبيق الذي تم إنشاؤه ، قد يكون من الصعب ملاحظة البعض الآخر حتى عند إجراء مراجعة التعليمات البرمجية من قبل فريق من المطورين ذوي الخبرة. ومع ذلك ، يحدث أيضًا أنه بسبب الإهمال أو سبب آخر ، يكتب المبرمج في بعض الأحيان ببساطة رمزًا غريبًا وغير منطقي ، والذي ، مع ذلك ، يبدو أنه يؤدي وظيفته بنجاح. ولكن أبعد من ذلك ، عند العودة إلى ما تم كتابته ، أو عندما يدرس الآخرون هذا الرمز ، تبدأ الأسئلة في الظهور والتي لا يمكن الإجابة عليها.

يمكن أن تتحول إعادة هيكلة الرمز القديم إلى مشاكل ، خاصة إذا كانت أجزاء أخرى من البرنامج تعتمد عليه ، لذلك عندما تجد حتى بعض الإنشاءات المنحنية بصراحة ، the هل يعمل؟ لا تلمسها! ″. وبالتالي ، فإن هذا يجعل من الصعب دراسة المصدر ، وبالتالي يصبح من الصعب توسيع القدرات المتاحة. انسداد قاعدة التعليمات البرمجية ، هناك احتمال أن بعض المشاكل الداخلية الصغيرة وغير المرئية ، ولكن من المحتمل أن تكون غير سارة للغاية ، لن يتم إصلاحها في الوقت المناسب.

في مرحلة ما ، سيتم الشعور بعواقب هذا الخطأ ، لكن بحثه سيستغرق الكثير من الوقت ، لأن شكوك المطور ستقع على عدد كبير من أجزاء الشفرة الغريبة التي لم يتم إعادة كتابتها في وقت واحد. ويترتب على ذلك أنه يجب تصحيح مختلف المشاكل والغرائب ​​في جزء معين فور كتابته. في حالة وجود أسباب معقولة لترك كل شيء كما هو (على سبيل المثال ، إذا كان الرمز مكتوبًا على أنه نوع من الفراغ ″ في وقت لاحق ″) ، فيجب أن يكون هذا الجزء مصحوبًا بتعليق توضيحي.

وتجدر الإشارة أيضًا إلى أنه بغض النظر عن مؤهلات المطور ، فإن بعض اللحظات الإشكالية وغير الناجحة يمكن أن تفلت من نظرته ، وفي بعض الأحيان يمكن تطبيق "حل مؤقت" في مكان ما ، والذي سيتم نسيانه قريبًا ويصبح "دائمًا". في وقت لاحق ، فإن تحليل مثل هذا الرمز (على الأرجح ، سيشارك مطور آخر في هذا) سوف يستغرق الكثير من الجهد غير المقبول.

في مثل هذه الحالات ، يمكن أن تساعد مراجعة التعليمات البرمجية. ومع ذلك ، إذا كانت المهمة خطيرة للغاية ، فإن هذا الخيار سيتطلب الكثير من الوقت. بالإضافة إلى ذلك ، عندما يكون هناك الكثير من الأخطاء أو العيوب الصغيرة ، فقد لا يلاحظ الفاحص وراءها أخطاء عالية المستوى. يصبح التحقق من الشفرة روتينًا مملًا ، وتنخفض فعالية المراجعة تدريجيًا.

من الواضح أن المهام الروتينية سيتم نقلها بشكل أفضل بكثير من شخص لآخر. يستخدم هذا النهج في العديد من مجالات الحداثة. أتمتة العمليات المختلفة هي مفتاح الازدهار. ما هو الأتمتة في سياق هذا الموضوع؟

يعد التحليل الثابت مساعدًا موثوقًا به في حل مشكلة كتابة رمز عمل معقول ومستقر. في كل مرة قبل إرسال نتائج أنشطتهم إلى المراجعة ، سيكون المبرمج قادرًا على إجراء فحص آلي ولن يثقل كاهل المطورين الآخرين (ونفسه) بالعمل غير الضروري. سيتم إرسال الرمز للتحقق فقط بعد أخذ جميع تحذيرات المحلل بعين الاعتبار: تم إصلاح الأخطاء ، وإعادة كتابة اللحظات الغريبة أو على الأقل شرحها بالتعليق.

بالطبع ، لا تختفي الحاجة إلى مراجعة التعليمات البرمجية ، ولكن التحليل الثابت يكمل وييسر تنفيذها بشكل كبير. سيتم إصلاح جزء كبير بما فيه الكفاية من الأخطاء بفضل المحلل ، ولن يتم نسيان اللحظات الغريبة وتمييزها وفقًا لذلك. وفقًا لذلك ، من خلال مراجعة الكود ، سيكون من الممكن التركيز على التحقق من صحة تنفيذ التفاعلات المنطقية المعقدة وإيجاد المشكلات الأساسية التي ، للأسف ، لا يمكن للمحلل اكتشافها (حتى الآن).

TensorFlow.NET




هذه المقالة مستوحاة من مشروع TensorFlow.NET. إنه يمثل تنفيذ القدرة على العمل مع وظائف مكتبة التعلم الآلي TensorFlow الشهيرة من خلال كود C # (بالمناسبة ، اختبرناها أيضًا ). بدت هذه الفكرة مثيرة للاهتمام تمامًا ، لأنه في وقت كتابة هذا التقرير ، كان العمل مع المكتبة متاحًا فقط في Python و Java و Go. يتم تحديث

شفرة المصدر المتاحة على GitHub باستمرار والآن حجمها يزيد قليلاً عن مائة ألف سطر. بعد دراسة سطحية ، كانت هناك رغبة في إجراء التحقق باستخدام التحليل الثابت. تم استخدام PVS-Studio كأداة محددة ، أثبتت فعاليتها في عدد كبير إلى حد ما من المشاريع المختلفة .


بالنسبة إلى TensorFlow.NET ، عرض المحلل عددًا من التحذيرات: 39 مستويات عالية و 227 مستويات متوسطة و 154 مستويات منخفضة (يمكنك القراءة حول مستويات التحذير هنا في القسم الفرعي "مستويات التحذير ومجموعات قواعد التشخيص"). من شأن التحليل التفصيلي لكل منها أن يجعل هذه المقالة عملاقة ، وبالتالي فإن تلك التي بدت أكثر إثارة للاهتمام بالنسبة لي موصوفة أدناه. من الجدير بالذكر أيضًا أن بعض المشكلات التي تم العثور عليها تمت مواجهتها عدة مرات في المشروع ، وأن تحليل كل جزء من التعليمات البرمجية يتجاوز نطاق هذه المقالة.

يضع المشروع نفسه على عاتقه مهمة جادة إلى حد ما ، ولسوء الحظ ، فإن ظهور أنواع مختلفة من أجزاء التعليمات البرمجية الغريبة أمر لا مفر منه. في هذه المقالة سأحاول أن أوضح أن استخدام التحليل الثابت يمكن أن يبسط عمل المبرمجين بشكل كبير ، مشيرًا إلى المجالات التي قد تسبب أسئلة. لا يشير التحذير دائمًا إلى وجود خطأ ، ولكنه غالبًا ما يشير إلى رمز قد يتسبب في تساؤلات في الشخص. وفقًا لذلك ، فإن احتمال إعادة تصميم رمز غريب إما ، أو التعليق عليه وفقًا لذلك ، يزداد بشكل ملحوظ.

شظايا جذبت الانتباه عند دراسة تقرير المحلل


في الواقع ، يمكن استدعاء عدد كبير إلى حد ما من تحذيرات المحللين لهذا المشروع ليس الكثير من الأخطاء ، بل بالأحرى رمز غريب. عند عرض الخطوط التي تم إصدار تحذير لها ، تنشأ حيرة على الأقل. بالطبع ، بعض الأمثلة أدناه هي على الأرجح "حلول مؤقتة" ، لكنهم لا يعطون أي تعليقات حول هذه المشكلة ، مما يعني أن الشخص الذي سيعمل مع هذا الرمز في المستقبل سيكون لديه أسئلة ، يبحث عن إجابات سيستغرق وقتًا إضافيًا.


في الوقت نفسه ، تشير بعض التحذيرات إلى رمز من الواضح أنه ليس غريبًا فحسب ، بل خاطئ فقط. هذا هو الخطر الرئيسي لـ `` كود غريب '' - من الصعب للغاية ملاحظة خطأ حقيقي إذا رأيت هنا وهناك حلول غير مفهومة وتعتاد تدريجيًا على حقيقة أن الرمز يبدو غير صحيح.

جولة جمع متطورة


private static void _RemoveDefaultAttrs(....)
{
  var producer_op_dict = new Dictionary<string, OpDef>();
  producer_op_list.Op.Select(op =>
  {
    producer_op_dict[op.Name] = op;
    return op;
  }).ToArray();           
  ....
}

تحذير المحلل : V3010 مطلوب القيمة المرجعة للدالة 'ToArray' لاستخدامها. importer.cs 218

يعتبر المحلل أن استدعاء Toray في هذه المرحلة أمر مريب ، لأن القيمة التي أرجعتها هذه الوظيفة لم يتم تعيينها لمتغير. ومع ذلك ، فإن مثل هذا الرمز ليس خطأ. وتستخدم هذه التركيبة لتجميع producer_op_dict القاموس مع القيم المقابلة لعناصر producer_op_list.Op القائمة . من الضروري استدعاء ToArray بحيث يتم استدعاء الدالة التي تم تمريرها كوسيطة إلى الأسلوب Select لجميع العناصر في المجموعة.

في رأيي ، لا يبدو الرمز الأفضل. ملء القاموس غير واضح إلى حد ما ، وقد يرغب بعض المطورين في إزالة استدعاء "غير ضروري" لـ ToArray . سيكون من الأسهل والأكثر قابلية للفهم استخدام حلقة foreach هنا :

var producer_op_dict = new Dictionary<string, OpDef>();

foreach (var op in producer_op_list.Op)
{
  producer_op_dict[op.Name] = op;
}

في هذه الحالة ، يبدو الرمز بسيطًا قدر الإمكان.

تبدو لحظة أخرى مماثلة على النحو التالي:

public GraphDef convert_variables_to_constants(....)
{
  ....
  inference_graph.Node.Select(x => map_name_to_node[x.Name] = x).ToArray();
  ....
}

تحذير المحلل : V3010 مطلوب القيمة المرجعة للدالة 'ToArray' لاستخدامها. graph_util_impl.cs 48

والفرق الوحيد هو أن مثل هذا الإدخال يبدو أكثر اختصارًا ، ولكن الإغراء لإزالة مكالمة ToArray هنا لا يختفي ، ولا يزال يبدو غير واضح.

حل مؤقت


public GraphDef convert_variables_to_constants(....)
{
  ....
  var source_op_name = get_input_name(node);
  while(map_name_to_node[source_op_name].Op == "Identity")
  {
    throw new NotImplementedException);
    ....
  }
  ....
}

تحذير المحلل : V3020 "رمية" غير مشروطة داخل حلقة. graph_util_impl.cs 73

في المشروع قيد النظر ، غالبًا ما يتم استخدام النهج التالي: إذا كان يجب تنفيذ بعض السلوك لاحقًا ، فسيتم طرح NotImplementedException في المكان المناسب . من الواضح لماذا يحذر المحلل من خطأ محتمل في هذه القطعة: استخدام بينما بدلاً من إذا لا يبدو معقولًا جدًا حقًا.

هذا ليس التحذير الوحيد الذي يظهر بسبب استخدام الحلول المؤقتة. على سبيل المثال ، هناك طريقة:

public static Tensor[] _SoftmaxCrossEntropyWithLogitsGrad(
  Operation op, Tensor[] grads
)
{
  var grad_loss = grads[0];
  var grad_grad = grads[1];
  var softmax_grad = op.outputs[1];
  var grad = _BroadcastMul(grad_loss, softmax_grad);

  var logits = op.inputs[0];
  if(grad_grad != null && !IsZero(grad_grad)) // <=
  {
    throw new NotImplementedException("_SoftmaxCrossEntropyWithLogitsGrad");
  }

  return new Tensor[] 
  {
    grad,
    _BroadcastMul(grad_loss, -nn_ops.log_softmax(logits))
  };
}

تحذير المحلل : تعبير V3022 'grad_grad! = Null &&! IsZero (grad_grad)' خطأ دائمًا. nn_grad.cs 93

في الواقع ، لن يتم طرح NotImplementedException ("_ SoftmaxCrossEntropyWithLogitsGrad") لأن هذا الرمز غير قابل للوصول. لكشف السبب ، يجب أن تذهب إلى رمز وظيفة IsZero :

private static bool IsZero(Tensor g)
{
  if (new string[] { "ZerosLike", "Zeros" }.Contains(g.op.type))
    return true;

  throw new NotImplementedException("IsZero");
}

الأسلوب إما إرجاع true أو يطرح استثناء. هذا الرمز ليس خطأ - من الواضح أن التطبيق هنا ترك لوقت لاحق. الشيء الرئيسي هو أن ″ إذن ″ وصل بالفعل. حسنًا ، اتضح بنجاح كبير أن PVS-Studio لن يسمح لك بنسيان وجود مثل هذا النقص هنا :)

هل Tensor هو Tensor؟


private static Tensor[] _ExtractInputShapes(Tensor[] inputs)
{
  var sizes = new Tensor[inputs.Length];
  bool fully_known = true;
  for(int i = 0; i < inputs.Length; i++)
  {
    var x = inputs[i];

    var input_shape = array_ops.shape(x);
    if (!(input_shape is Tensor) || input_shape.op.type != "Const")
    {
      fully_known = false;
      break;
    }

    sizes[i] = input_shape;
  }
  ....
}

تحذير المحلل : V3051 فحص نوع مفرط. الكائن من نوع "Tensor" بالفعل. array_grad.cs 154

نوع الإرجاع لطريقة الشكل هو Tensor . لذا ، فإن إدخال الإدخال هو فحص Tensor يبدو غريبًا على الأقل. ربما ، بمجرد أن تعيد الطريقة قيمة من نوع مختلف ويصبح التحقق منطقيًا ، ولكن من الممكن أيضًا أنه بدلاً من Tensor ، يجب أن تحدد الحالة نوعًا من الوريث لهذه الفئة. بطريقة أو بأخرى ، يجب على المطور الانتباه إلى هذا الجزء.

فحص شامل


public static Tensor[] _BaseFusedBatchNormGrad(....)
{
  ....
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  var results = grad_fun(new FusedBatchNormParams
  {
    YBackprop = grad_y,
    X = x,
    Scale = scale,
    ReserveSpace1 = pop_mean,
    ReserveSpace2 = pop_var,
    ReserveSpace3 = version == 2 ? op.outputs[5] : null,
    Epsilon = epsilon,
    DataFormat = data_format,
    IsTraining = is_training
  });

  var (dx, dscale, doffset) = (results[0], results[1], results[2]);
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  ....
}

تحذيرات محلل :

  • V3021 يوجد عبارتان 'if' مع تعبيرات شرطية متطابقة. تحتوي العبارة 'if' الأولى على طريقة الإرجاع. هذا يعني أن العبارة 'if' الثانية هي nn_grad.cs 230 عديمة المعنى
  • V3022 Expression 'data_format == "NCHW"' خطأ دائمًا. nn_grad.cs 247

على عكس بعض الأمثلة السابقة ، من الواضح أن هناك خطأ ما في التعليمات البرمجية. الفحص الثاني لا معنى له ، لأنه إذا كان الشرط صحيحًا ، فإن تنفيذ البرنامج لن يصل إليه على الإطلاق. ربما يُسمح ببعض الأخطاء المطبعية هنا ، أو أن أحد الفحوصات غير ضرورية بشكل عام.

وهم الاختيار


public Tensor Activate(Tensor x, string name = null)
{
  ....
  Tensor negative_part;
  if (Math.Abs(_threshold) > 0.000001f)
  {
    negative_part = gen_ops.relu(-x + _threshold);
  } else
  {
    negative_part = gen_ops.relu(-x + _threshold);
  }
  ....
}

تحذير المحلل : V3004 العبارة "ثم" تعادل العبارة "آخر". gen_nn_ops.activations.cs 156

عرض مسلي إلى حد ما لفعالية استخدام التحليل الثابت في التنمية. من الصعب التوصل إلى سبب معقول وراء كتابة المطور لهذا الرمز المحدد - على الأرجح ، هذا خطأ نموذجي في النسخ واللصق (على الرغم من أن هذا ، بالطبع ، قد يكون آخر ″ في وقت لاحق ″).

هناك أجزاء أخرى من خطة مماثلة ، على سبيل المثال:

private static Operation _GroupControlDeps(
  string dev, Operation[] deps, string name = null
)
{
  return tf_with(ops.control_dependencies(deps), ctl =>
  {
    if (dev == null)
    {
      return gen_control_flow_ops.no_op(name);
    }
    else
    {
      return gen_control_flow_ops.no_op(name);
    }
  });
}

تحذير المحلل : V3004 العبارة "ثم" تعادل العبارة "آخر". control_flow_ops.cs 135

ربما بمجرد أن يصبح الشيك منطقيًا ، ولكنه اختفى بمرور الوقت ، أو تم التخطيط لبعض التغييرات الإضافية في المستقبل. ومع ذلك ، لا يبدو أن أيًا من هذين الخيارين يعد مبررًا كافيًا لترك شيء مشابه في التعليمات البرمجية ، دون توضيح هذه الغرابة بأي شكل من الأشكال. مع درجة عالية من الاحتمال ، تم إجراء خطأ النسخ واللصق بنفس الطريقة تمامًا.

الشيك المتأخر


public static Tensor[] Input(int[] batch_shape = null,
  TF_DataType dtype = TF_DataType.DtInvalid,
  string name = null,
  bool sparse = false,
  Tensor tensor = null)
{
  var batch_size = batch_shape[0];
  var shape = batch_shape.Skip(1).ToArray(); // <=

  InputLayer input_layer = null;
  if (batch_shape != null)                   // <=
    ....
  else
    ....

  ....
}

تحذير المحلل : V3095 تم استخدام كائن "batch_shape" قبل التحقق من صحته. التحقق من الخطوط: 39 ، 42. keras.layers.cs 39

خطأ كلاسيكي وخطير إلى حد ما بشأن الاستخدام المحتمل لمتغير ، وهو رابط إلى أي مكان. في هذه الحالة ، يشير الرمز صراحة إلى احتمال أن يكون batch_shape فارغًا - وهذا واضح من كل من قائمة الحجج ومن التحقق اللاحق للمتغير نفسه. وبالتالي ، يشير المحلل هنا إلى خطأ واضح.

آخر ″ لوقت لاحق ″؟


public MnistDataSet(
  NDArray images, NDArray labels, Type dataType, bool reshape // <=
) 
{
  EpochsCompleted = 0;
  IndexInEpoch = 0;

  NumOfExamples = images.shape[0];

  images = images.reshape(
    images.shape[0], images.shape[1] * images.shape[2]
  );
  images = images.astype(dataType);
  // for debug np.multiply performance
  var sw = new Stopwatch();
  sw.Start();
  images = np.multiply(images, 1.0f / 255.0f);
  sw.Stop();
  Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
  Data = images;

  labels = labels.astype(dataType);
  Labels = labels;
}

تحذير المحلل : V3117 معلمة المُنشئ ' reshape ' غير مستخدمة. MnistDataSet.cs 15

مثل بعض الشذوذ الأخرى ، هذا على الأرجح يرجع إلى حقيقة أن الوظيفة بعيدة عن التنفيذ الكامل ومن المحتمل جدًا أن يتم استخدام معلمة إعادة التشكيل بطريقة أو بأخرى في هذا المُنشئ في المستقبل. ولكن في الوقت الحاضر ، يبدو كما لو أنه تم طرحه هنا بهذه الطريقة. إذا تم ذلك حقًا ″ في وقت لاحق ″ ، فمن الحكمة وضع علامة على ذلك ببعض التعليقات. إذا لم يكن الأمر كذلك ، فقد اتضح أن الشفرة التي تنشئ الكائن ستضطر إلى إلقاء معلمة إضافية في المنشئ ، ومن المحتمل جدًا أنه سيكون من الملائم أكثر عدم القيام بذلك.

بعيد المنال ممكن الاشارة


public static Tensor[] _GatherV2Grad(Operation op, Tensor[] grads)
{
  ....
  if((int)axis_static == 0)
  {
    var params_tail_shape = params_shape.slice(new NumSharp.Slice(start:1));
    var values_shape = array_ops.concat(
      new[] { indices_size, params_tail_shape }, 0
    );
    var values = array_ops.reshape(grad, values_shape);
    indices = array_ops.reshape(indices, indices_size);
    return new Tensor[]
    {
      new IndexedSlices(values, indices, params_shape), // <=
      null,
      null
    };
  }
  ....
}

تحذير المحلل : V3146 احتمال عدم الإشارة إلى قيمة "القيم" في الوسيطة الأولى للوسيطة الأولى. يمكن أن يُرجع "_outputs.FirstOrDefault ()" قيمة فارغة افتراضية. array_grad.cs 199

لفهم ماهية المشكلة ، يجب عليك أولاً الانتقال إلى رمز مُنشئ الشرائح المفهرس :

public IndexedSlices(
  Tensor values, Tensor indices, Tensor dense_shape = null
)
{
  _values = values;
  _indices = indices;
  _dense_shape = dense_shape;

  _values.Tag = this; // <=
}

من الواضح أن تمرير قيمة فارغة إلى هذا المُنشئ سيؤدي إلى استثناء. ومع ذلك ، لماذا يعتبر المحلل أن متغير القيم قد يحتوي على قيمة خالية ؟

يستخدم PVS-Studio تقنية تحليل تدفق البيانات ، والتي تتيح لك العثور على مجموعات القيم المحتملة للمتغيرات في أجزاء مختلفة من التعليمات البرمجية. يخبرك تحذير بأنه يمكن إرجاع قيمة خالية في المتغير المحدد بالسطر التالي: _outputs.FirstOrDefault () . ومع ذلك ، بالنظر إلى الكود أعلاه ، يمكنك أن تجد أن قيمة متغير القيم يتم الحصول عليها عن طريق استدعاء array_ops.reshape (grad، القيم_شكل) . فأين _outputs.FirstOrDefault () إذن ؟

والحقيقة هي أنه عند تحليل دفق البيانات ، لا يتم النظر فقط في الوظيفة الحالية ، ولكن يتم استدعاؤها أيضًا ؛ يحصل PVS-Studio على معلومات حول مجموعة القيم المحتملة لأي متغير في أي مكان. وبالتالي ، فإن التحذير يعني أن تطبيق array_ops.reshape (grad، value_shape) يحتوي على استدعاء لـ _outputs.FirstOrDefault () ، والتي يتم إرجاع نتيجتها في النهاية.

للتحقق من ذلك ، انتقل إلى تطبيق إعادة التشكيل :

public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
            => gen_array_ops.reshape(tensor, shape, null);

ثم انتقل إلى طريقة إعادة التشكيل التي تسمى بالداخل:
public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
{
  var _op = _op_def_lib._apply_op_helper(
    "Reshape", name, new { tensor, shape }
  );
  return _op.output;
}

تُرجع الدالة _apply_op_helper كائنًا من فئة العملية يحتوي على خاصية الإخراج . عند استلام قيمته يسمى الرمز الموصوف في التحذير:

public Tensor output => _outputs.FirstOrDefault();

Tensor هو بالطبع نوع مرجعي ، لذلك ستكون القيمة الافتراضية له فارغة . من كل هذا يمكن ملاحظة أن PVS-Studio يحلل بدقة البنية المنطقية للكود ، ويخترق عمق بنية المكالمات.

أكمل المحلل عمله ، مشيراً إلى مكان يحتمل أن يكون مشكلة. يجب على المبرمج التحقق مما إذا كان قد ينشأ موقف عندما تكون العناصر في المخرجات غير موجودة.

وبالتالي ، فإن التحليل الثابت سيجبر المطور على الأقل على الانتباه إلى الجزء المريب من أجل تقييم ما إذا كان الخطأ قد يحدث بالفعل هناك. باستخدام هذا النهج ، سيتم تقليل عدد الأخطاء التي لم تتم ملاحظتها بسرعة.

انتظار غير موثوق به؟


private (LoopVar<TItem>, Tensor[]) _BuildLoop<TItem>(
  ....
) where ....
{
  ....
  // Finds the closest enclosing non-None control pivot.
  var outer_context = _outer_context;
  object control_pivot = null;
  while (outer_context != null && control_pivot == null) // <=
  {

  }

  if (control_pivot != null)
  {

  }
  ....
}

تحذير المحلل : V3032 لا يمكن الاعتماد على انتظار هذا التعبير ، حيث أن المترجم قد يحسن بعض المتغيرات. استخدم متغير (متغيرات) متغيرة أو بدائية التزامن لتجنب ذلك. whileContext.cs 212

يشير المحلل إلى أن مثل هذا التطبيق للتوقعات يمكن تحسينه من قبل المترجم ، لكني أشك في أنهم حاولوا حقًا تنفيذ الانتظار هنا - على الأرجح ، ببساطة لم تتم إضافة الشفرة وسيتم تطويرها في المستقبل. قد يكون من المفيد طرح استثناء غير مُطبق هنا ، نظرًا لأن هذه الممارسة تُستخدم في مكان آخر من المشروع. بطريقة أو بأخرى ، أعتقد أنه من الواضح أن بعض التعليقات التوضيحية لن تضر.

كسر الحدود


public TensorShape(int[][] dims)
{
  if(dims.Length == 1)
  {
    switch (dims[0].Length)
    {
      case 0: shape = new Shape(new int[0]); break;
      case 1: shape = Shape.Vector((int)dims[0][0]); break;
      case 2: shape = Shape.Matrix(dims[0][0], dims[1][2]); break; // <=
      default: shape = new Shape(dims[0]); break;
    }
  }
  else
  {
    throw new NotImplementedException("TensorShape int[][] dims");
  }
}

تحذير المحلل : V3106 من المحتمل أن يكون المؤشر خارج النطاق . يشير مؤشر "1" إلى ما وراء حدود "التعتيم". TensorShape.cs 107 من

بين المقتطفات الغريبة من الشفرة التي رأيتها ، كان هناك خطأ حقيقي يصعب ملاحظته . الجزء التالي خاطئ هنا: dims [1] [2] . من الواضح أن الحصول على عنصر باستخدام الفهرس 1 من مصفوفة عنصر واحد خطأ. في نفس الوقت ، إذا قمت بتغيير الجزء إلى خافت [0] [2] ، يظهر خطأ آخر - الحصول على عنصر مع الفهرس 2 من الصفيف [0] ، طوله في هذه الحالة هو الفرع 2. وبالتالي ، تحولت هذه المشكلة إلى كما لو كان مع "قاع مزدوج".

في أي حال ، يجب دراسة جزء الشفرة هذا وتصحيحه من قبل المطور. في رأيي ، هذا المثال هو توضيح ممتاز لأداء تحليل تدفق البيانات في PVS-Studio.

أولباتكا؟


private void _init_from_args(object initial_value = null, ....) // <=
{
  var init_from_fn = initial_value.GetType().Name == "Func`1"; // <=
  ....
  tf_with(...., scope =>
  {
    ....
    tf_with(...., delegate
    {
      initial_value = ops.convert_to_tensor(  // <=
        init_from_fn ? (initial_value as Func<Tensor>)():initial_value,
        name: "initial_value",
        dtype: dtype
      );
    });
    _shape = shape ?? (initial_value as Tensor).TensorShape;
    _initial_value = initial_value as Tensor; // <=
    ....
    _dtype = _initial_value.dtype.as_base_dtype(); // <=

    if (_in_graph_mode)
    {
      ....

      if (initial_value != null) // <=
      {
        ....
      }

      ....
    }

    ....
  });
}

لفهم الشفرة أعلاه ، يجدر أيضًا تنفيذ تطبيق وظيفة tf_with:

[DebuggerStepThrough] // with "Just My Code" enabled this lets the 
[DebuggerNonUserCode()]  //debugger break at the origin of the exception
public static void tf_with<T>(
  T py, Action<T> action
) where T : ITensorFlowObject
{
  try
  {
    py.__enter__();
    action(py);
  }
  finally
  {
    py.__exit__();
    py.Dispose();
  }
}

تحذير المحلل : V3019 ربما تتم مقارنة متغير غير صحيح بالصفر بعد تحويل النوع باستخدام الكلمة الأساسية "as". تحقق من المتغيرات "الأولي_قيمة" ، "القيمة الأولية". ResourceVariable.cs 137

_init_from_args هي دالة ضخمة إلى حد ما ، لذلك تم حذف العديد من الأجزاء. يمكنك رؤيته بالكامل بالضغط على الرابط . على الرغم من أن التحذير في البداية لم يبدو جادًا حقًا بالنسبة لي ، بعد الدراسة ، أدركت أن هناك خطأ ما في الرمز.

أولاً ، تجدر الإشارة إلى أنه يمكن استدعاء الطريقة بدون تمرير المعلمات وستكون فارغة افتراضيًا في القيمة الأولية . في هذه الحالة ، سيتم طرح استثناء على السطر الأول. ثانياً ، التحقق

القيمة الأولية إلى عدم المساواة فارغة تبدو غريبة: إذا أصبحت القيمة الأولية فعلاً فارغة بعد استدعاء ops.convert_to_tensor ، فإن القيمة الأولية ستكون فارغة ، مما يعني أن استدعاء _initial_value.dtype.as_base_dtype () سيؤدي أيضًا إلى استثناء.

تلميحات المحلل التي قد تحتاج إلى التحقق من وجود قيمة خالية هي _initial_value ، ولكن كما هو موضح أعلاه ، فإن الإشارة إلى هذا المتغير تحدث قبل هذا الاختبار ، لذلك سيكون هذا الخيار أيضًا غير صحيح.

هل يمكن ملاحظة هذا الخطأ الصغير في مثل هذه الوظيفة العملاقة بدون PVS-Studio؟ أنا أشك في ذلك كثيرا.

استنتاج


في مشروع يحتوي على الكثير من الأمثلة للرموز الغريبة ، يمكن إخفاء الكثير من المشاكل. المبرمج ، الذي اعتاد على رؤية المفهوم ، يتوقف عن ملاحظة الأخطاء في نفس الوقت. يمكن أن تكون العواقب محزنة للغاية. في الواقع ، من بين تحذيرات المحللين هناك أيضًا تحذيرات خاطئة ، ومع ذلك ، في معظم الحالات ، تشير التحذيرات على الأقل إلى أجزاء من التعليمات البرمجية التي يمكن أن تسبب أسئلة عندما ينظر إليها شخص. في حالة كتابة الرمز الغريب عمدًا ، يجدر ترك التفسيرات بحيث تكون القطعة واضحة للمطور الذي سيعمل مع هذا الرمز في المستقبل (حتى لو كان ذلك يعني ترك التعليقات لنفسه).

في الوقت نفسه ، يمكن لأدوات التحليل الثابتة ، مثل PVS-Studio ، أن تساعد بشكل كبير في العثور على الأخطاء والشذوذ المحتمل بحيث تكون مرئية ولا يتم نسيانها ، ويتم بعد ذلك تحسين جميع الحلول المؤقتة وتحويلها إلى عمل نظيف ومنظم ومستقر الرمز.


إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Nikita Lipilin. كيف يخفي رمز غريب الأخطاء؟ تحليل TensorFlow.NET .

All Articles