Wie seltsamer Code verbirgt Fehler? TensorFlow.NET-Analyse

TensorFlow.NET und PVS-Studio

Die statische Analyse ist ein äußerst nützliches Werkzeug für jeden Entwickler, da sie hilft, nicht nur Fehler, sondern auch verdächtige und seltsame Codeteile rechtzeitig zu finden, die für Programmierer, die in Zukunft mit ihm arbeiten müssten, Verwirrung stiften können. Diese Idee wird durch eine Analyse des offenen C # -Projekts von TensorFlow.NET demonstriert, das für die Zusammenarbeit mit der beliebten TensorFlow-Bibliothek für maschinelles Lernen entwickelt wird.

Ich heiße Nikita Lipilin. Vor einiger Zeit bin ich in die C # -Programmiererabteilung von PVS-Studio eingetreten. Traditionell schreiben alle Neulinge im Team Artikel, in denen die Ergebnisse der Überprüfung verschiedener offener Projekte mit dem statischen Analysegerät PVS-Studio erläutert werden. Solche Artikel helfen neuen Mitarbeitern, das Produkt besser kennenzulernen, und bieten gleichzeitig zusätzliche Vorteile bei der Verbreitung der statischen Analysemethode. Ich schlage vor, dass Sie sich mit meiner ersten Arbeit zum Thema Analyse offener Projekte vertraut machen.

Einführung


Die Vielzahl möglicher Fehler im Programmcode ist erstaunlich. Einige von ihnen zeigen sich sofort bei einer Oberflächenuntersuchung der erstellten Anwendung, andere können selbst bei einer Codeüberprüfung durch ein Team erfahrener Entwickler schwer zu bemerken sein. Es kommt jedoch auch vor, dass der Programmierer aus Unaufmerksamkeit oder aus anderen Gründen manchmal einfach seltsamen und unlogischen Code schreibt, der seine Funktion jedoch (scheinbar) erfolgreich erfüllt. Aber nur weiter, wenn wir zu dem zurückkehren, was geschrieben wurde, oder wenn andere diesen Code studieren, tauchen Fragen auf, die nicht beantwortet werden können.

Das Refactoring von altem Code kann zu Problemen führen, insbesondere wenn andere Teile des Programms davon abhängen. Wenn Sie also selbst einige offen gekrümmte Konstruktionen finden, funktioniert das "Funktioniert es?" Fass es nicht an! ″. In der Folge macht es dies schwierig, die Quelle zu untersuchen, und daher wird es schwierig, die verfügbaren Fähigkeiten zu erweitern. Die Codebasis ist verstopft. Es besteht die Wahrscheinlichkeit, dass ein kleines und unsichtbares, aber möglicherweise sehr unangenehmes internes Problem nicht rechtzeitig behoben wird.

Irgendwann werden die Konsequenzen dieses Fehlers zu spüren sein, aber seine Suche wird viel Zeit in Anspruch nehmen, da der Verdacht des Entwicklers auf eine große Anzahl seltsamer Codefragmente fällt, die nicht gleichzeitig überarbeitet wurden. Daraus folgt, dass verschiedene Probleme und Kuriositäten in einem bestimmten Fragment unmittelbar nach seinem Schreiben behoben werden sollten. In dem Fall, in dem es vernünftige Gründe gibt, alles so zu belassen, wie es ist (z. B. wenn der Code als eine Art Leerzeichen "für später" geschrieben ist), sollte einem solchen Fragment ein erläuternder Kommentar beigefügt werden.

Es ist auch erwähnenswert, dass unabhängig von der Qualifikation des Entwicklers einige problematische und einfach erfolglose Momente aus seinem Blick verschwinden können und manchmal an einer Stelle eine „vorübergehende Lösung“ angewendet werden kann, die bald vergessen wird und „dauerhaft“ wird. Anschließend wird die Analyse eines solchen Codes (höchstwahrscheinlich wird ein anderer Entwickler daran beteiligt sein) inakzeptabel viel Aufwand erfordern.

In solchen Fällen kann die Codeüberprüfung hilfreich sein. Wenn die Aufgabe jedoch sehr ernst ist, benötigt diese Option viel Zeit. Wenn viele kleine Fehler oder Mängel vorliegen, bemerkt der Prüfer möglicherweise keine Fehler auf hoher Ebene. Das Überprüfen des Codes wird zu einer mühsamen Routine, und die Wirksamkeit der Überprüfung nimmt allmählich ab.

Offensichtlich werden Routineaufgaben viel besser von Person zu Computer übertragen. Dieser Ansatz wird in vielen Bereichen der Moderne angewendet. Die Automatisierung verschiedener Prozesse ist der Schlüssel zum Wohlstand. Was ist Automatisierung im Kontext dieses Themas?

Ein zuverlässiger Assistent bei der Lösung des Problems, vernünftigen und stabilen Arbeitscode zu schreiben, ist die statische Analyse. Jedes Mal, bevor der Programmierer die Ergebnisse seiner Aktivitäten an die Überprüfung sendet, kann er eine automatisierte Überprüfung durchführen und andere Entwickler (und sich selbst) nicht mit unnötiger Arbeit belasten. Der Code wird erst zur Überprüfung gesendet, nachdem alle Warnungen des Analysators berücksichtigt wurden: Fehler wurden behoben und seltsame Momente wurden neu geschrieben oder zumindest durch einen Kommentar erklärt.

Natürlich verschwindet die Notwendigkeit der Codeüberprüfung nicht, aber die statische Analyse ergänzt und vereinfacht die Implementierung erheblich. Ein ausreichend großer Teil der Fehler wird dank des Analysators behoben, und seltsame Momente werden definitiv nicht vergessen und entsprechend markiert. Dementsprechend wird es bei einer Codeüberprüfung möglich sein, sich darauf zu konzentrieren, die Richtigkeit der Implementierung komplexer logischer Interaktionen zu überprüfen und zugrunde liegende Probleme zu finden, die vom Analysator (bisher) leider nicht erkannt werden können.

TensorFlow.NET




Dieser Artikel ist vom TensorFlow.NET-Projekt inspiriert. Es stellt die Implementierung der Fähigkeit dar, mit der Funktionalität der beliebten TensorFlow-Bibliothek für maschinelles Lernen über C # -Code zu arbeiten (wir haben sie übrigens auch getestet ). Diese Idee schien ziemlich interessant zu sein, da zum Zeitpunkt des Schreibens die Arbeit mit der Bibliothek nur in Python, Java und Go verfügbar ist.

Der auf GitHub verfügbare Quellcode wird ständig aktualisiert und hat jetzt ein Volumen von etwas mehr als hunderttausend Zeilen. Nach einer oberflächlichen Untersuchung bestand der Wunsch, eine Überprüfung mittels statischer Analyse durchzuführen. PVS-Studio wurde als spezifisches Tool verwendet, das sich in einer relativ großen Anzahl verschiedener Projekte bewährt hat .


Für TensorFlow.NET zeigte der Analysator eine Reihe von Warnungen an: 39 Hohe Stufen, 227 Mittlere Stufen und 154 Niedrige Stufen (Informationen zu Warnstufen finden Sie hier im Unterabschnitt „Warnstufen und Diagnoseregelsätze“). Eine detaillierte Analyse von jedem von ihnen würde diesen Artikel gigantisch machen, daher werden im Folgenden nur diejenigen beschrieben, die mir am interessantesten erschienen. Es ist auch erwähnenswert, dass einige der gefundenen Probleme mehrmals im Projekt auftreten und die Analyse jedes solchen Codes über den Rahmen dieses Artikels hinausgeht.

Das Projekt stellt sich einer ziemlich ernsten Aufgabe, und leider ist das Auftreten verschiedener Arten von seltsamen Codefragmenten unvermeidlich. In diesem Artikel werde ich versuchen zu zeigen, dass die Verwendung der statischen Analyse die Arbeit von Programmierern erheblich vereinfachen kann, indem ich auf Bereiche verweise, die Fragen verursachen können. Nicht immer weist eine Warnung auf einen Fehler hin, aber häufig weist sie auf einen Code hin, der bei einer Person Fragen verursachen würde. Dementsprechend ist die Wahrscheinlichkeit, dass ein seltsamer Code entweder neu gestaltet oder auf jeden Fall entsprechend kommentiert wird, erheblich erhöht.

Fragmente, die beim Studium des Analysatorberichts Aufmerksamkeit erregt haben


Tatsächlich kann eine ziemlich große Anzahl von Analysatorwarnungen für dieses Projekt nicht als so viele Fehler, sondern als seltsamer Code bezeichnet werden. Beim Betrachten von Zeilen, für die eine Warnung ausgegeben wird, tritt zumindest Verwirrung auf. Natürlich sind einige der folgenden Beispiele wahrscheinlich "vorübergehende Lösungen", aber sie geben keine Kommentare zu diesem Problem ab. Dies bedeutet, dass jemand, der in Zukunft mit diesem Code arbeiten wird, Fragen hat und nach Antworten sucht wird zusätzliche Zeit in Anspruch nehmen.


Gleichzeitig weisen einige Warnungen auf Code hin, der offensichtlich nicht nur seltsam, sondern auch falsch ist. Dies ist die Hauptgefahr von "seltsamem Code" - es ist äußerst schwierig, einen echten Fehler zu bemerken, wenn Sie hier und da seltsame Lösungen sehen und sich allmählich daran gewöhnen, dass der Code falsch zu sein scheint.

Anspruchsvolle Sammeltour


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();           
  ....
}

Analysator Warnung : V3010 Der Rückgabewert der Funktion 'ToArray' muss verwendet werden. importer.cs 218

Der Analysator betrachtet den Aufruf von Toray an dieser Stelle als verdächtig, da der von dieser Funktion zurückgegebene Wert keiner Variablen zugewiesen ist. Ein solcher Code ist jedoch kein Fehler. Dieses Konstrukt wird verwendet, um das Wörterbuch Producer_op_dict mit Werten zu füllen, die den Elementen der Liste Producer_op_list.Op entsprechen . Ein Aufruf von ToArray ist erforderlich, damit die als Argument an die Select- Methode übergebene Funktion für alle Elemente in der Auflistung aufgerufen wird.

Meiner Meinung nach sieht der Code nicht gut aus. Das Ausfüllen des Wörterbuchs ist etwas offensichtlich, und einige Entwickler möchten möglicherweise den "unnötigen" Aufruf von ToArray entfernen . Es wäre viel einfacher und verständlicher, die foreach-Schleife hier zu verwenden :

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

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

In diesem Fall sieht der Code so einfach wie möglich aus.

Ein anderer ähnlicher Moment sieht so aus:

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

Analysator Warnung : V3010 Der Rückgabewert der Funktion 'ToArray' muss verwendet werden. graph_util_impl.cs 48

Der einzige Unterschied besteht darin, dass ein solcher Datensatz prägnanter aussieht, aber nur die Versuchung, den ToArray- Aufruf hier zu entfernen, nicht verschwindet und immer noch nicht offensichtlich ist.

Vorübergehende Lösung


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);
    ....
  }
  ....
}

Analyzer-Warnung : V3020 Ein bedingungsloser "Wurf" innerhalb einer Schleife. graph_util_impl.cs 73

In dem betrachteten Projekt wird häufig der folgende Ansatz verwendet: Wenn ein bestimmtes Verhalten später implementiert werden sollte, wird an der entsprechenden Stelle eine NotImplementedException ausgelöst . Es ist klar, warum der Analysator vor einem möglichen Fehler in diesem Artikel warnt: Die Verwendung von while anstelle von if sieht nicht wirklich vernünftig aus.

Dies ist nicht die einzige Warnung, die aufgrund der Verwendung temporärer Lösungen angezeigt wird. Zum Beispiel gibt es eine Methode:

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))
  };
}

Analysator Warnung : V3022 Ausdruck 'grad_grad! = Null &&! IsZero (grad_grad)' ist immer falsch. nn_grad.cs 93

Tatsächlich wird eine NotImplementedException ("_ SoftmaxCrossEntropyWithLogitsGrad") niemals ausgelöst, da dieser Code einfach nicht erreichbar ist. Um den Grund zu ermitteln, müssen Sie zum Code der IsZero- Funktion gehen :

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

  throw new NotImplementedException("IsZero");
}

Die Methode gibt entweder true zurück oder löst eine Ausnahme aus. Dieser Code ist kein Fehler - die Implementierung hier bleibt natürlich für später. Die Hauptsache ist, dass "dann" wirklich angekommen ist. Nun, es hat sich sehr erfolgreich herausgestellt, dass PVS-Studio Sie nicht vergessen lässt, dass es hier eine solche Unvollkommenheit gibt :)

Ist 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;
  }
  ....
}

Analysator Warnung : V3051 Eine übermäßige Typprüfung. Das Objekt ist bereits vom Typ 'Tensor'. array_grad.cs 154

Der Rückgabetyp des Formverfahrens ist Tensor . Die input_shape ist also Tensor Check sieht zumindest komisch aus. Sobald die Methode einen Wert eines anderen Typs zurückgegeben hat und die Überprüfung sinnvoll war, ist es möglicherweise auch möglich, dass die Bedingung anstelle von Tensor eine Art Erbe dieser Klasse angibt. Auf die eine oder andere Weise sollte der Entwickler auf dieses Fragment achten.

Gründliche Überprüfung


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("");

  ....
}

Warnungen des Analysators :

  • V3021 Es gibt zwei ' if' -Anweisungen mit identischen bedingten Ausdrücken. Die erste 'if'-Anweisung enthält die Methodenrückgabe. Dies bedeutet, dass die zweite 'if'-Anweisung sinnlos ist. Nn_grad.cs 230
  • V3022 Der Ausdruck 'data_format == "NCHW"' ist immer falsch. nn_grad.cs 247

Im Gegensatz zu einigen der vorherigen Beispiele stimmt der Code eindeutig nicht. Die zweite Prüfung macht keinen Sinn, da die Ausführung des Programms die Bedingung überhaupt nicht erreicht, wenn sie erfüllt ist. Vielleicht ist hier ein Tippfehler erlaubt, oder eine der Prüfungen ist im Allgemeinen überflüssig.

Die Illusion der Wahl


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);
  }
  ....
}

Analyzer-Warnung : V3004 Die Anweisung 'then' entspricht der Anweisung 'else'. gen_nn_ops.activations.cs 156

Eine ziemlich amüsante Demonstration der Wirksamkeit der Verwendung statischer Analysen in der Entwicklung. Es ist schwierig, einen vernünftigen Grund zu finden, warum der Entwickler diesen bestimmten Code geschrieben hat - höchstwahrscheinlich handelt es sich um einen typischen Fehler beim Kopieren und Einfügen (obwohl dies natürlich ein weiterer "für später" sein kann).

Es gibt andere Fragmente eines ähnlichen Plans, zum Beispiel:

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);
    }
  });
}

Analyzer-Warnung : V3004 Die Anweisung 'then' entspricht der Anweisung 'else'. control_flow_ops.cs 135

Vielleicht hat die Überprüfung einmal Sinn gemacht, aber im Laufe der Zeit ist sie verschwunden, oder es sind weitere Änderungen in der Zukunft geplant. Weder die eine noch die andere Option scheint jedoch eine ausreichende Rechtfertigung dafür zu sein, etwas Ähnliches im Code zu belassen, ohne diese Seltsamkeit in irgendeiner Weise zu erklären. Mit hoher Wahrscheinlichkeit wurde ein Kopier- und Einfügefehler genauso gemacht.

Verspäteter Scheck


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
    ....

  ....
}

Analyzer-Warnung : V3095 Das Objekt 'batch_shape' wurde verwendet, bevor es gegen null verifiziert wurde. Überprüfen Sie die Zeilen: 39, 42. keras.layers.cs 39

Ein klassischer und ziemlich gefährlicher Fehler bei der möglichen Verwendung einer Variablen, die eine Verbindung zu nirgendwo darstellt. In diesem Fall bedeutet der Code explizit die Möglichkeit , dass batch_shape wird null - das ergibt sich sowohl aus der Liste der Argumente und der anschließenden Überprüfung der gleichen Variablen. Somit zeigt der Analysator hier einen eindeutigen Fehler an.

Noch ein "für später"?


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;
}

Analysator Warnung : V3117 Der Konstruktorparameter ' Umformen ' wird nicht verwendet. MnistDataSet.cs 15

Wie bei einigen anderen Kuriositäten ist dies höchstwahrscheinlich auf die Tatsache zurückzuführen, dass die Funktionalität bei weitem nicht vollständig implementiert ist und es durchaus möglich ist, dass der Umformungsparameter in Zukunft in diesem Konstruktor irgendwie verwendet wird. Aber vorerst scheint es, als würde er einfach so hierher geworfen. Wenn dies wirklich "für später" getan wird, wäre es ratsam, dies mit einem Kommentar zu markieren. Wenn nicht, stellt sich heraus, dass der Code, der das Objekt erstellt, einen zusätzlichen Parameter in den Konstruktor werfen muss, und es ist durchaus bequemer, dies nicht zu tun.

Schwer fassbare mögliche Null-Dereferenzierung


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
    };
  }
  ....
}

Analyzer Warnung : V3146 Mögliche Null-Dereferenzierung des ersten Arguments 'Werte' innerhalb der Methode. Der Wert '_outputs.FirstOrDefault ()' kann den Standardwert null zurückgeben. array_grad.cs 199

Um zu verstehen, wo das Problem liegt, sollten Sie sich zunächst dem Konstruktorcode indexedSlices zuwenden :

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

  _values.Tag = this; // <=
}

Wenn Sie diesem Konstruktor null übergeben , wird offensichtlich eine Ausnahme ausgelöst. Doch warum hält der Analysator , dass die Werte Variable enthalten null ?

PVS-Studio verwendet die Datenflussanalysetechnik, mit der Sie die Sätze möglicher Werte von Variablen in verschiedenen Teilen des Codes finden können. Eine Warnung weist Sie darauf hin, dass null in der angegebenen Variablen mit der folgenden Zeile zurückgegeben werden kann: _outputs.FirstOrDefault () . Allerdings Blick auf den Code oben, können Sie den Wert der feststellen , dass Werte Variable wird durch den Aufruf erhalten array_ops.reshape (grad, values_shape) . Woher kommt dann _outputs.FirstOrDefault () ?

Tatsache ist, dass bei der Analyse des Datenstroms nicht nur die aktuelle Funktion berücksichtigt wird, sondern auch alle aufgerufen werden. PVS-Studio erhält überall Informationen über die möglichen Werte einer Variablen. Eine Warnung bedeutet also, dass die Implementierung von array_ops.reshape (grad, values_shape) einen Aufruf von _outputs.FirstOrDefault () enthält , dessen Ergebnis letztendlich zurückgegeben wird.

Um dies zu überprüfen, gehen Sie zur Umformungsimplementierung :

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

Gehen Sie dann zu der Umformungsmethode , die im Inneren aufgerufen wird:
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;
}

Die Funktion _apply_op_helper gibt ein Objekt der Operation- Klasse zurück , das die Ausgabeeigenschaft enthält . Nach Erhalt seines Wertes wird der in der Warnung beschriebene Code aufgerufen:

public Tensor output => _outputs.FirstOrDefault();

Tensor ist natürlich ein Referenztyp, daher ist der Standardwert dafür null . Aus all dem ist ersichtlich, dass PVS-Studio die logische Struktur des Codes akribisch analysiert und tief in die Struktur von Aufrufen eindringt.

Der Analysator hat seine Arbeit abgeschlossen und weist auf einen möglicherweise problematischen Ort hin. Der Programmierer muss prüfen, ob eine Situation auftreten kann, wenn Elemente in _outputs fehlen.

Somit wird die statische Analyse den Entwickler zumindest zwingen, auf das verdächtige Fragment zu achten, um zu beurteilen, ob der Fehler dort tatsächlich auftreten kann. Mit diesem Ansatz wird die Anzahl der Fehler, die unbemerkt bleiben, schnell reduziert.

Unzuverlässiges Warten?


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)
  {

  }
  ....
}

Analyzer-Warnung : V3032 Das Warten auf diesen Ausdruck ist unzuverlässig, da der Compiler möglicherweise einige der Variablen optimiert. Verwenden Sie flüchtige Variablen oder Synchronisationsprimitive, um dies zu vermeiden. WhileContext.cs 212

Der Analysator gibt an, dass eine solche Implementierung des Wartens vom Compiler optimiert werden kann, aber ich bezweifle, dass sie wirklich versucht haben, das Warten hier zu implementieren - höchstwahrscheinlich wurde der Code einfach nicht hinzugefügt und wird in Zukunft weiterentwickelt. Es könnte sich lohnen, hier eine NotImplementedException auszulösen , da diese Vorgehensweise an anderer Stelle im Projekt verwendet wird. Auf die eine oder andere Weise glaube ich, dass ein erklärender Kommentar offensichtlich nicht schaden würde.

Grenzen brechen


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");
  }
}

Analyzer Warnung : V3106 Möglicherweise ist der Index nicht gebunden. Der '1'-Index zeigt über die' dims'-Grenze hinaus. TensorShape.cs 107

Unter den seltsamen Codefragmenten, die ich gesehen habe, gab es einen echten Fehler, der sehr schwer zu bemerken ist. Das folgende Fragment ist hier fehlerhaft: dims [1] [2] . Das Abrufen eines Elements mit Index 1 aus einem Array eines Elements ist offensichtlich ein Fehler. Wenn Sie das Fragment in dims [0] [2] ändern , tritt gleichzeitig ein weiterer Fehler auf: Abrufen eines Elements mit Index 2 aus dem Array dims [0] , dessen Länge in diesem Fall 2 beträgt. Somit stellte sich dieses Problem heraus als ob mit einem "doppelten Boden".

In jedem Fall sollte dieses Codefragment vom Entwickler untersucht und korrigiert werden. Meiner Meinung nach ist dieses Beispiel ein hervorragendes Beispiel für die Leistung der Datenflussanalyse in PVS-Studio.

Olepatka?


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) // <=
      {
        ....
      }

      ....
    }

    ....
  });
}

Um den obigen Code zu verstehen, lohnt es sich auch, die Implementierung der Funktion tf_with einzuführen:

[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();
  }
}

Analyzer Warnung : V3019 Möglicherweise wird eine falsche Variable nach der Typkonvertierung mit dem Schlüsselwort 'as' mit null verglichen. Überprüfen Sie die Variablen 'initial_value', '_initial_value'. ResourceVariable.cs 137

_init_from_args ist eine ziemlich umfangreiche Funktion, daher wurden viele Fragmente weggelassen. Sie können es vollständig sehen, indem Sie auf den Link klicken . Obwohl mir die Warnung zunächst nicht wirklich ernst erschien, stellte ich nach dem Studium fest, dass mit dem Code definitiv etwas nicht stimmte.

Zunächst sei darauf hingewiesen, dass das Verfahren ohne Parameterübergabe aufgerufen werden kann und wird standardmäßig es wird null in initial_value . In diesem Fall wird in der ersten Zeile eine Ausnahme ausgelöst. Zweitens die Überprüfung

initial_value auf die Null - Ungleichheit sieht seltsam aus : wenn initial_value wirklich wird null nach dem Aufruf ops.convert_to_tensor , dann _initial_value wäre null , was bedeutet , dass Aufruf _initial_value.dtype.as_base_dtype () wäre auch eine Ausnahme werfen.

Der Analysator weist darauf hin, dass Sie möglicherweise prüfen müssen, ob null _initial_value ist. Wie oben erwähnt, erfolgt die Bezugnahme auf diese Variable jedoch vor diesem Test, sodass diese Option ebenfalls falsch wäre.

Wäre dieser kleine Fehler in einer so riesigen Funktion ohne PVS-Studio aufgefallen? Ich bezweifle es sehr.

Fazit


In einem Projekt mit vielen Beispielen für seltsamen Code können viele Probleme ausgeblendet werden. Der Programmierer, der sich daran gewöhnt, das Unverständliche zu sehen, bemerkt gleichzeitig keine Fehler mehr. Die Folgen können sehr traurig sein. In der Tat gibt es unter den Warnungen des Analysators auch falsche. In den meisten Fällen weisen Warnungen jedoch zumindest auf Codefragmente hin, die beim Betrachten durch eine Person Fragen verursachen können. In dem Fall, in dem der seltsame Code absichtlich geschrieben wurde, lohnt es sich, Erklärungen zu hinterlassen, damit das Fragment dem Entwickler klar ist, der in Zukunft mit diesem Code arbeiten wird (auch wenn dies bedeutet, Kommentare für sich selbst zu hinterlassen).

Gleichzeitig können statische Analysewerkzeuge wie PVS-Studio eine große Hilfe sein, um potenzielle Fehler und Kuriositäten zu finden, damit sie sichtbar und nicht vergessen werden. Alle temporären Lösungen werden anschließend verfeinert und in ein sauberes, strukturiertes und stabiles Funktionieren umgewandelt der Code.


Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Nikita Lipilin. Wie verbirgt seltsamer Code Fehler? TensorFlow.NET-Analyse .

All Articles