Xây dựng biểu đồ trong C++

Trình tạo biểu đồ C++ là một công cụ mạnh mẽ để:

  • Xây dựng biểu đồ phức tạp
  • Các biểu đồ tham số (ví dụ: cài đặt đại biểu trên InferenceCalculator, bật/tắt các phần của biểu đồ)
  • Loại bỏ biểu đồ trùng lặp (ví dụ: thay vì biểu đồ dành riêng cho CPU và GPU trong pbtxt bạn có thể có một mã xây dựng các đồ thị bắt buộc, chia sẻ nhất có thể)
  • Hỗ trợ đầu vào/đầu ra biểu đồ tuỳ chọn
  • Tuỳ chỉnh biểu đồ theo nền tảng

Cách sử dụng cơ bản

Hãy xem cách sử dụng trình tạo biểu đồ C++ cho một biểu đồ đơn giản:

# 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 {} }
    }
  }
}

Hàm tạo CalculatorGraphConfig ở trên có thể có dạng như sau:

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

Tóm tắt ngắn:

  • Sử dụng Graph::In/SideIn để nhận dữ liệu đầu vào biểu đồ dưới dạng Stream/SidePacket
  • Dùng Node::Out/SideOut để nhận dữ liệu đầu ra của nút dưới dạng Stream/SidePacket
  • Dùng Stream/SidePacket::ConnectTo để kết nối luồng và gói phụ với đầu vào nút (Node::In/SideIn) và đầu ra biểu đồ (Graph::Out/SideOut)
    • Có một "lối tắt" toán tử >> mà bạn có thể sử dụng thay cho Hàm ConnectTo (ví dụ: x >> node.In("IN")).
  • Stream/SidePacket::Cast dùng để truyền luồng phát hoặc gói phụ của AnyType (ví dụ: Stream<AnyType> in = graph.In(0);) cho một loại cụ thể
    • Việc sử dụng các loại thực tế thay vì AnyType sẽ giúp bạn có một đường dẫn tốt hơn cho khai thác các chức năng của trình tạo biểu đồ và cải thiện biểu đồ của bạn dễ đọc.

Cách sử dụng nâng cao

Hàm hiệu dụng

Hãy trích xuất mã xây dựng suy luận thành một hàm số hiệu dụng chuyên dụng để giúp dễ đọc và sử dụng lại mã:

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

Do đó, RunInference cung cấp một giao diện rõ ràng cho biết đâu là đầu vào/đầu ra và các loại của chúng.

Có thể dễ dàng tái sử dụng, ví dụ: bạn chỉ cần nhập một vài dòng nếu muốn chạy thêm suy luận mô hình:

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

Và bạn không cần sao chép tên và thẻ (InferenceCalculator, TENSORS, MODEL) hoặc giới thiệu các hằng số riêng ở đây – những thông tin chi tiết được bản địa hoá thành hàm RunInference.

Hạng tiện ích

Và chắc chắn, không chỉ về chức năng, trong một số trường hợp, sẽ có lợi khi giới thiệu các lớp tiện ích có thể giúp tạo mã xây dựng biểu đồ dễ đọc hơn và ít gặp lỗi hơn.

MediaPipe cung cấp công cụ tính PassThroughCalculator, chỉ cần vượt qua thông qua dữ liệu đầu vào:

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

Hãy xem mã xây dựng C++ đơn giản để tạo biểu đồ trên:

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

Mặc dù việc biểu diễn pbtxt có thể dễ gặp lỗi (khi chúng ta có nhiều dữ liệu đầu vào để truyền qua), mã C++ trông còn tệ hơn: các thẻ trống và lệnh gọi Cast lặp lại. Hãy xem cách chúng tôi có thể làm tốt hơn bằng việc giới thiệu 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_;
};

Và giờ đây, mã xây dựng biểu đồ có thể có dạng như:

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

Giờ đây, bạn không thể có thứ tự hoặc chỉ mục không chính xác trong quá trình tạo bản dựng và lưu một số nội dung nhập bằng cách đoán loại cho Cast trong PassThrough đầu vào.

Những việc nên làm và việc không nên làm

Xác định đầu vào biểu đồ ngay từ đầu nếu có thể

Trong mã bên dưới:

  • Có thể khó đoán số lượng dữ liệu đầu vào mà bạn có trong biểu đồ.
  • Nhìn chung, có thể dễ gặp lỗi và khó duy trì trong tương lai (ví dụ: chỉ mục chính xác không? tên? điều gì sẽ xảy ra nếu một số thông tin đầu vào bị xoá hoặc đặt thành không bắt buộc? v.v).
  • Việc sử dụng lại RunSomething bị hạn chế vì các biểu đồ khác có thể có giá trị đầu vào

KHÔNG NÊN — ví dụ về mã không hợp lệ.

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

Thay vào đó, hãy xác định dữ liệu đầu vào biểu đồ ngay từ đầu trình tạo biểu đồ:

NÊN — ví dụ về mã hiệu quả.

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

Sử dụng std::optional nếu bạn có luồng đầu vào hoặc gói phụ luôn được xác định và đặt ngay từ đầu:

NÊN — ví dụ về mã hiệu quả.

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

Xác định đầu ra của biểu đồ ở cuối

Trong mã bên dưới:

  • Có thể khó đoán số đầu ra bạn có trong biểu đồ.
  • Nhìn chung, có thể dễ gặp lỗi và khó duy trì trong tương lai (ví dụ: chỉ mục chính xác không? tên? điều gì sẽ xảy ra nếu một số thói quen xấu bị loại bỏ hoặc đặt thành không bắt buộc? v.v).
  • Việc sử dụng lại RunSomething bị hạn chế vì các biểu đồ khác có thể có kết quả khác nhau

KHÔNG NÊN — ví dụ về mã không hợp lệ.

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

Thay vào đó, hãy xác định kết quả biểu đồ ở cuối trình tạo biểu đồ:

NÊN — ví dụ về mã hiệu quả.

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

Giữ tách các nút khỏi nhau

Trong MediaPipe, luồng gói và gói phụ có ý nghĩa như quá trình xử lý nút. Và bất kỳ yêu cầu đầu vào và sản phẩm đầu ra nào của nút đều được thể hiện rõ ràng độc lập về luồng và gói phụ mà nó sử dụng và tạo ra.

KHÔNG NÊN — ví dụ về mã không hợp lệ.

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

Trong mã trên:

  • Các nút được kết nối với nhau, ví dụ: node4 biết dữ liệu đầu vào của nó ở đâu đến từ (node1, node2, node3) và việc này khiến việc tái cấu trúc trở nên phức tạp, bảo trì và sử dụng lại mã
    • Mẫu sử dụng như vậy là cách hạ cấp từ cách biểu diễn proto, trong đó các nút sẽ được tách riêng theo mặc định.
  • Các lệnh gọi node#.Out("OUTPUT") bị trùng lặp và khả năng đọc sẽ bị ảnh hưởng khi bạn có thể sử dụng tên rõ ràng hơn, đồng thời cung cấp một loại thực tế.

Vì vậy, để khắc phục các vấn đề trên, bạn có thể viết mã tạo biểu đồ sau:

NÊN — ví dụ về mã hiệu quả.

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

Bây giờ, nếu cần, bạn có thể dễ dàng xoá node1 và biến b thành dữ liệu đầu vào biểu đồ và không cần cập nhật cho node2, node3, node4 (tương tự như ở dạng biểu diễn proto nhân tiện), vì chúng tách rời nhau.

Nhìn chung, mã trên sao chép biểu đồ proto chặt chẽ hơn:

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"

Ngoài ra, giờ đây bạn có thể trích xuất các hàm số hiệu dụng để sử dụng lại trong các biểu đồ khác:

NÊN — ví dụ về mã hiệu quả.

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

Phân tách các nút để dễ đọc hơn

KHÔNG NÊN — ví dụ về mã không hợp lệ.

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

Trong mã trên, bạn có thể khó nắm bắt ý tưởng nơi mỗi nút bắt đầu và kết thúc. Để cải thiện điều này và giúp trình đọc mã, bạn chỉ cần để trống dòng trước và sau mỗi nút:

NÊN — ví dụ về mã hiệu quả.

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

Ngoài ra, cách trình bày ở trên khớp với CalculatorGraphConfig proto tốt hơn.

Nếu bạn trích xuất các nút vào hàm hiệu dụng, thì các nút đó sẽ nằm trong phạm vi của các hàm hoạt động đã được xác định rõ ràng nơi bắt đầu và kết thúc, do vậy hoàn toàn không có vấn đề gì có:

NÊN — ví dụ về mã hiệu quả.

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