0

I have read about data flow graph and dependence graph from the Intel TBB Tutorial, and feel a bit confusing about these two concepts.

Can I say that the key difference between data flow graph and dependence graph is whether there are explicitly shared resources or not?

But it seems that we can implement a dependence graph using function_node with pseudo messages, or implement a data flow graph using continue_node with shared global variables.

Anton
  • 6,349
  • 1
  • 25
  • 53
oneg
  • 38
  • 5

1 Answers1

3

The difference between a function_node accepting a continue_msg input and a continue_node is the behavior when receiving a message. This is a consequence of the concept of "dependence graph."

The idea of dependence graphs is that the only information being passed by the graph is the completion of a task. If you have four tasks (A,B,C,D) all operating on the same shared data, and tasks A and B must be complete before either C or D can be started, you define four continue_nodes, and attach the output of node A to C and D, and the same for B. You may also create a broadcast_node<continue_msg> and attach A and B as successors to it. (The data being used in the computation must be accessible by some other means.)

example dependence graph

To start the graph you do a try_put of a continue_msg to the broadcast_node. The broadcast_node sends a continue_msg to each successor (A & B).

continue_nodes A and B each have 1 predecessor (the broadcast_node.) On receiving a number of continue_msgs equal to their predecessor count (1), they are queued to execute, using and updating the data representing the state of the computation.

When continue_node A completes, it sends a continue_msg to each successor, C & D. Those nodes each have two predecessors, so they do not execute on receiving this message. They only remember they have received one message.

When continue_node B completes, it also sends a continue_msg to C and D. This will be the second continue_msg each node receives, so tasks will be queued to execute their function_bodies.

continue_nodes use the graph only to express this order. No data is transferred from node to node (beyond the signal that a predecessor is complete.)

If the nodes in the graph were function_nodes accepting continue_msgs rather than continue_nodes, the reaction to the broadcast_node getting a continue_msg would be

  1. The broadcast_node would forward a continue_msg to A and B, and they would each execute their function_bodies.
  2. Node A would complete, and pass continue_msgs to C and D.
  3. On receiving the continue_msg, tasks would be queued to execute the function_bodies of C and D.
  4. Node B would complete execution, and forward a continue_msg to C and D.
  5. C and D, on receiving this second continue_msg, would queue a task to execute their function_bodies a second time.

Notice 3. above. The function_node reacts each time it receives a continue_msg. The continue_node knows how many predecessors it has, and only reacts when it receives a number of continue_msgs equal to its number of predecessors.

The dependence graph is convenient if there is a lot of state being used in the computation, and if the sequence of tasks is well understood. The idea of "shared state" does not necessarily dictate the use of a dependence graph, but a dependence graph cannot pass anything but completion state of the work involved, and so must use shared state to communicate other data.

(Note that the completion order I am describing above is only one possible ordering. Node B could complete before node A, but the sequence of actions would be similar.)

cahuson
  • 826
  • 4
  • 10
  • Thanks for your comprehensive answer! You mean if we have a lot of messages to be passed between `function_node`, `continue_node` with shared memory will be a better choice? b.t.w. tasks are queued in both `step 3` and `step 5`, it this duplicate? – oneg Jan 31 '15 at 05:34
  • It isn't a duplicate; C and D would be executed twice if they are `function_nodes` (once for each message received.) If they are `continue_nodes`, they have to receive a number of messages equal to the number of predecessors (well, not exactly; you can specify an intial count for the predecessors, which would increase the number of messages they'd have to receive.) – cahuson Feb 01 '15 at 20:26
  • If you think of your program in terms of "I cannot do C or D until A and B are complete,", then `continue_nodes` are an easier way to formulate a solution. For instance, people creating animation software might use them to specify that the leg of an actor must have its location and orientation determined, and the thing the actor is standing on must be located, before the foot can be computed. In this case the node calculating the ground position and the node calculating the leg position might both have the foot node as a successor. Some developers have used the `continue_node` this way. – cahuson Feb 01 '15 at 22:56
  • If you have a series of actions you perform on a set of data (an image buffer, for example), you might want to marshal the buffers using pointers, and pass the pointer from node to node. In this case, you would use a `function_node` accepting a buffer pointer and passing it on to its successors. The image processing examples we've done use this setup. – cahuson Feb 01 '15 at 22:56
  • In both cases, the system state is a global entity. We create `continue_nodes` in the animation example to enforce the order that parts of the state are computed. In the image processing example, the pointer to the image buffer tells the function_node which buffer to work on, but the buffer generally would be considered part of the global state. The data passed node-to-node in `flow::graph` is copied, so passing more than pointers or small data like `doubles` is expensive. – cahuson Feb 01 '15 at 22:56
  • These examples are helpful for understanding, I think it is clear for me now. Thanks a lot :P – oneg Feb 03 '15 at 07:11