Ndërtimi i grafikëve në C++

Ndërtuesi i grafikëve C++ është një mjet i fuqishëm për:

  • Ndërtimi i grafikëve komplekse
  • Parametizimi i grafikëve (p.sh. vendosja e një delegati në InferenceCalculator , aktivizimi/çaktivizimi i pjesëve të grafikut)
  • Zhdukja e grafikëve (p.sh. në vend të grafikëve të dedikuar për CPU dhe GPU në pbtxt, ju mund të keni një kod të vetëm që ndërton grafikët e kërkuar, duke ndarë sa më shumë që të jetë e mundur)
  • Mbështetja e hyrjeve/daljeve opsionale të grafikut
  • Përshtatja e grafikëve për platformë

Përdorimi bazë

Le të shohim se si ndërtuesi i grafikut C++ mund të përdoret për një grafik të thjeshtë:

# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"

# Graph outputs.
output_stream: "output_tensors"

node {
  calculator: "InferenceCalculator"
  input_stream: "TENSORS:input_tensors"
  input_side_packet: "MODEL:model"
  output_stream: "TENSORS:output_tensors"
  options: {
    [drishti.InferenceCalculatorOptions.ext] {
      # Requesting GPU delegate.
      delegate { gpu {} }
    }
  }
}

Funksioni për të ndërtuar CalculatorGraphConfig të mësipërm mund të duket si:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  // Requesting GPU delegate.
  inference_opts.mutable_delegate()->mutable_gpu();
  input_tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  Stream<std::vector<Tensor>> output_tensors =
      inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Përmbledhje e shkurtër:

  • Përdorni Graph::In/SideIn për të marrë hyrjet e grafikut si Stream/SidePacket
  • Përdorni Node::Out/SideOut për të marrë daljet e nyjeve si Stream/SidePacket
  • Përdorni Stream/SidePacket::ConnectTo për të lidhur transmetimet dhe paketat anësore me hyrjet e nyjeve ( Node::In/SideIn ) dhe daljet e grafikut ( Graph::Out/SideOut )
    • Ekziston një operator "shortcut" >> që mund ta përdorni në vend të funksionit ConnectTo (P.sh. x >> node.In("IN") ).
  • Stream/SidePacket::Cast përdoret për të transmetuar transmetimin ose paketën anësore të AnyType (p.sh. Stream<AnyType> in = graph.In(0); ) në një lloj të caktuar
    • Përdorimi i llojeve aktuale në vend të AnyType ju vendos në një rrugë më të mirë për të çliruar aftësitë e ndërtuesit të grafikëve dhe për të përmirësuar lexueshmërinë e grafikëve tuaj.

Përdorimi i Avancuar

Funksionet e shërbimeve

Le të nxjerrim kodin e ndërtimit të konkluzionit në një funksion të dedikuar të shërbimeve për të ndihmuar në lexueshmërinë dhe ripërdorimin e kodit:

// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
    Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
    const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  *inference_opts.mutable_delegate() = delegate;
  tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  InferenceCalculatorOptions::Delegate delegate;
  delegate.mutable_gpu();
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

Si rezultat, RunInference ofron një ndërfaqe të qartë që tregon se cilat janë hyrjet/daljet dhe llojet e tyre.

Mund të ripërdoret lehtësisht, p.sh. janë vetëm disa rreshta nëse dëshironi të ekzekutoni një përfundim shtesë të modelit:

  // Run first inference.
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);
  // Run second inference on the output of the first one.
  Stream<std::vector<Tensor>> extra_output_tensors =
      RunInference(output_tensors, extra_model, delegate, graph);

Dhe nuk keni nevojë të kopjoni emra dhe etiketa ( InferenceCalculator , TENSORS , MODEL ) ose të prezantoni konstante të dedikuara aty-këtu - këto detaje lokalizohen në funksionin RunInference .

Klasat e shërbimeve

Dhe me siguri, nuk ka të bëjë vetëm me funksionet, në disa raste është e dobishme të prezantohen klasat e shërbimeve që mund të ndihmojnë në bërjen e kodit të ndërtimit të grafikut më të lexueshëm dhe më pak të prirur për gabime.

MediaPipe ofron kalkulatorin PassThroughCalculator , i cili thjesht po kalon përmes hyrjeve të tij:

input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"

output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"

node {
  calculator: "PassThroughCalculator"
  input_stream: "float_value"
  input_stream: "int_value"
  input_stream: "bool_value"
  # The order must be the same as for inputs (or you can use explicit indexes)
  output_stream: "passed_float_value"
  output_stream: "passed_int_value"
  output_stream: "passed_bool_value"
}

Le të shohim kodin e drejtpërdrejtë të ndërtimit C++ për të krijuar grafikun e mësipërm:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  auto& pass_node = graph.AddNode("PassThroughCalculator");
  float_value.ConnectTo(pass_node.In("")[0]);
  int_value.ConnectTo(pass_node.In("")[1]);
  bool_value.ConnectTo(pass_node.In("")[2]);
  Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
  Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
  Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Ndërsa përfaqësimi pbtxt mund të jetë i prirur për gabime (kur kemi shumë hyrje për të kaluar), kodi C++ duket edhe më keq: etiketa të përsëritura boshe dhe thirrje Cast . Le të shohim se si mund të bëjmë më mirë duke prezantuar një PassThroughNodeBuilder :

class PassThroughNodeBuilder {
 public:
  explicit PassThroughNodeBuilder(Graph& graph)
      : node_(graph.AddNode("PassThroughCalculator")) {}

  template <typename T>
  Stream<T> PassThrough(Stream<T> stream) {
    stream.ConnectTo(node_.In(index_));
    return node_.Out(index_++).Cast<T>();
  }

 private:
  int index_ = 0;
  GenericNode& node_;
};

Dhe tani kodi i ndërtimit të grafikut mund të duket si:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  PassThroughNodeBuilder pass_node_builder(graph);
  Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
  Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
  Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Tani nuk mund të kesh renditje ose indeks të pasaktë në kodin e ndërtimit të kalimit dhe të ruash disa shkrime duke hamendësuar llojin për Cast nga hyrja PassThrough .

Të bësh dhe të mos bësh

Përcaktoni hyrjet e grafikut që në fillim nëse është e mundur

Në kodin më poshtë:

  • Mund të jetë e vështirë të merret me mend se sa inpute keni në grafik.
  • Mund të jetë i prirur për gabime në përgjithësi dhe i vështirë për t'u ruajtur në të ardhmen (p.sh. a është një indeks i saktë? Emri? Po sikur disa inpute të hiqen ose të bëhen opsionale? etj.).
  • Ripërdorimi i RunSomething është i kufizuar sepse grafikët e tjerë mund të kenë hyrje të ndryshme

MOS - shembull i kodit të keq.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();  // Bad.
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  // 10/100/N lines of code.
  Stream<B> b = graph.In(1).SetName("b").Cast<B>()  // Bad.
  Stream<D> d = RunSomething(a, b, graph);
  // ...

  return graph.GetConfig();
}

Në vend të kësaj, përcaktoni hyrjet tuaja të grafikut që në fillim të ndërtuesit tuaj të grafikut:

DO - shembull i kodit të mirë.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  Stream<B> b = graph.In(1).SetName("b").Cast<B>();
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();

  // 10/100/N lines of code.
  Stream<D> d = RunSomething(a, b, c, graph);
  // ...

  return graph.GetConfig();
}

Përdorni std::optional nëse keni një rrymë hyrëse ose paketë anësore që nuk është gjithmonë e përcaktuar dhe vendoseni në fillim:

DO - shembull i kodit të mirë.

std::optional<Stream<A>> a;
if (needs_a) {
  a = graph.In(0).SetName(a).Cast<A>();
}

Përcaktoni rezultatet e grafikut në fund

Në kodin më poshtë:

  • Mund të jetë e vështirë të merret me mend se sa rezultate keni në grafik.
  • Mund të jetë i prirur për gabime në përgjithësi dhe i vështirë për t'u mbajtur në të ardhmen (p.sh. a është një indeks i saktë? Emri? Po nëse disa dalje hiqen ose bëhen opsionale? etj.).
  • Ripërdorimi i RunSomething është i kufizuar pasi grafikët e tjerë mund të kenë rezultate të ndryshme

MOS - shembull i kodit të keq.

void RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  node.Out("OUTPUT_F")
      .SetName("output_f").ConnectTo(graph.Out(2));  // Bad.
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  node.Out("OUTPUT_D")
      .SetName("output_d").ConnectTo(graph.Out(0));  // Bad.
  // 10/100/N lines of code.
  node.Out("OUTPUT_E")
      .SetName("output_e").ConnectTo(graph.Out(1));  // Bad.
  // 10/100/N lines of code.
  RunSomething(input, graph);
  // ...

  return graph.GetConfig();
}

Në vend të kësaj, përcaktoni rezultatet e grafikut tuaj në fund të ndërtuesit tuaj të grafikut:

DO - shembull i kodit të mirë.

Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  return node.Out("OUTPUT_F").Cast<F>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
  // 10/100/N lines of code.
  Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
  // 10/100/N lines of code.
  Stream<F> f = RunSomething(input, graph);
  // ...

  // Outputs.
  d.SetName("output_d").ConnectTo(graph.Out(0));
  e.SetName("output_e").ConnectTo(graph.Out(1));
  f.SetName("output_f").ConnectTo(graph.Out(2));

  return graph.GetConfig();
}

Mbani nyjet të shkëputura nga njëra-tjetra

Në MediaPipe, rrjedhat e paketave dhe paketat anësore janë po aq kuptimplote sa nyjet e përpunimit. Dhe çdo kërkesë hyrëse e nyjeve dhe produkte dalëse shprehen qartë dhe në mënyrë të pavarur në termat e rrymave dhe paketave anësore që konsumon dhe prodhon.

MOS - shembull i kodit të keq.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));

  auto& node2 = graph.AddNode("Calculator2");
  node1.Out("OUTPUT").ConnectTo(node2.In("INPUT"));  // Bad.

  auto& node3 = graph.AddNode("Calculator3");
  node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C"));  // Bad.

  auto& node4 = graph.AddNode("Calculator4");
  node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C"));  // Bad.
  node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D"));  // Bad.

  // Outputs.
  node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0));  // Bad.
  node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1));  // Bad.
  node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2));  // Bad.
  node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3));  // Bad.

  return graph.GetConfig();
}

Në kodin e mësipërm:

  • Nyjet janë të lidhura me njëra-tjetrën, p.sh. node4 e di se nga vijnë hyrjet e saj ( node1 , node2 , node3 ) dhe e ndërlikon rifaktorimin, mirëmbajtjen dhe ripërdorimin e kodit
    • Një model i tillë përdorimi është një ulje nga përfaqësimi proto, ku nyjet janë të shkëputura si parazgjedhje.
  • node#.Out("OUTPUT") janë të dyfishta dhe lexueshmëria vuan pasi mund të përdorni emra më të pastër në vend të tyre dhe gjithashtu të siguroni një lloj aktual.

Pra, për të rregulluar problemet e mësipërme, mund të shkruani kodin e mëposhtëm të ndërtimit të grafikut:

DO - shembull i kodit të mirë.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  // `node1` usage is limited to 3 lines below.
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  // `node2` usage is limited to 3 lines below.
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  // `node3` usage is limited to 4 lines below.
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  // `node4` usage is limited to 5 lines below.
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Tani, nëse është e nevojshme, mund të hiqni lehtësisht node1 dhe ta bëni b një hyrje grafiku dhe nuk nevojiten përditësime për node2 , node3 , node4 (njëlloj si në paraqitjen proto), sepse ato janë të shkëputura nga njëra-tjetra.

Në përgjithësi, kodi i mësipërm përsërit më afër grafikun e protokollit:

input_stream: "a"

node {
  calculator: "Calculator1"
  input_stream: "INPUT:a"
  output_stream: "OUTPUT:b"
}

node {
  calculator: "Calculator2"
  input_stream: "INPUT:b"
  output_stream: "OUTPUT:C"
}

node {
  calculator: "Calculator3"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  output_stream: "OUTPUT:d"
}

node {
  calculator: "Calculator4"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  input_stream: "INPUT_D:d"
  output_stream: "OUTPUT:e"
}

output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"

Për më tepër, tani mund të nxirrni funksione të shërbimeve për ripërdorim të mëtejshëm në grafikë të tjerë:

DO - shembull i kodit të mirë.

Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
  auto& node = graph.AddNode("Calculator1");
  a.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<B>();
}

Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
  auto& node = graph.AddNode("Calculator2");
  b.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<C>();
}

Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
  auto& node = graph.AddNode("Calculator3");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  return node.Out("OUTPUT").Cast<D>();
}

Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
  auto& node = graph.AddNode("Calculator4");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  d.ConnectTo(node.In("INPUT_D"));
  return node.Out("OUTPUT").Cast<E>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Ndarja e nyjeve për lexueshmëri më të mirë

MOS - shembull i kodit të keq.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();
  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Në kodin e mësipërm, mund të jetë e vështirë të kuptosh idenë se ku fillon dhe mbaron secila nyje. Për ta përmirësuar këtë dhe për të ndihmuar lexuesit tuaj të kodit, thjesht mund të keni rreshta bosh para dhe pas çdo nyje:

DO - shembull i kodit të mirë.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Gjithashtu, përfaqësimi i mësipërm përputhet më mirë me përfaqësimin proto CalculatorGraphConfig .

Nëse nxirrni nyje në funksionet e shërbimeve, ato tashmë janë të shtrira brenda funksioneve dhe është e qartë se ku fillojnë dhe ku mbarojnë, kështu që është plotësisht mirë të keni:

DO - shembull i kodit të mirë.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}