Cpp-Taskflow  2.3.0
flow_builder.hpp
1 #pragma once
2 
3 #include "task.hpp"
4 
5 namespace tf {
6 
13 class FlowBuilder {
14 
15  friend class Task;
16 
17  public:
18 
24  FlowBuilder(Graph& graph);
25 
35  template <typename C>
36  Task emplace(C&& callable);
37 
47  template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>* = nullptr>
48  auto emplace(C&&... callables);
49 
56  Task composed_of(Taskflow& taskflow);
57 
75  template <typename I, typename C>
76  std::pair<Task, Task> parallel_for(I beg, I end, C&& callable, size_t chunk=1);
77 
95  template <
96  typename I,
97  typename C,
98  std::enable_if_t<std::is_arithmetic_v<I>, void>* = nullptr
99  >
101  I beg, I end, I step, C&& callable, size_t chunk = 1
102  );
103 
121  template <typename I, typename T, typename B>
122  std::pair<Task, Task> reduce(I beg, I end, T& result, B&& bop);
123 
139  template <typename I, typename T>
140  std::pair<Task, Task> reduce_min(I beg, I end, T& result);
141 
157  template <typename I, typename T>
158  std::pair<Task, Task> reduce_max(I beg, I end, T& result);
159 
181  template <typename I, typename T, typename B, typename U>
182  std::pair<Task, Task> transform_reduce(I beg, I end, T& result, B&& bop, U&& uop);
183 
208  template <typename I, typename T, typename B, typename P, typename U>
210  I beg, I end, T& result, B&& bop1, P&& bop2, U&& uop
211  );
212 
218  Task placeholder();
219 
226  void precede(Task A, Task B);
227 
233  void linearize(std::vector<Task>& tasks);
234 
241 
248  void broadcast(Task A, std::vector<Task>& others);
249 
257 
264  void gather(std::vector<Task>& others, Task A);
265 
272  void gather(std::initializer_list<Task> others, Task A);
273 
274  private:
275 
276  Graph& _graph;
277 
278  template <typename L>
279  void _linearize(L&);
280 };
281 
282 // Constructor
283 inline FlowBuilder::FlowBuilder(Graph& graph) :
284  _graph {graph} {
285 }
286 
287 // ----------------------------------------------------------------------------
288 
294 class Subflow : public FlowBuilder {
295 
296  public:
297 
301  template <typename... Args>
302  Subflow(Args&&... args);
303 
307  void join();
308 
312  void detach();
313 
317  bool detached() const;
318 
322  bool joined() const;
323 
324  private:
325 
326  bool _detached {false};
327 };
328 
329 // Constructor
330 template <typename... Args>
331 Subflow::Subflow(Args&&... args) :
332  FlowBuilder {std::forward<Args>(args)...} {
333 }
334 
335 // Procedure: join
336 inline void Subflow::join() {
337  _detached = false;
338 }
339 
340 // Procedure: detach
341 inline void Subflow::detach() {
342  _detached = true;
343 }
344 
345 // Function: detached
346 inline bool Subflow::detached() const {
347  return _detached;
348 }
349 
350 // Function: joined
351 inline bool Subflow::joined() const {
352  return !_detached;
353 }
354 
355 // ----------------------------------------------------------------------------
356 // Member definition of FlowBuilder
357 // ----------------------------------------------------------------------------
358 
359 // Function: emplace
360 template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>*>
361 auto FlowBuilder::emplace(C&&... cs) {
362  return std::make_tuple(emplace(std::forward<C>(cs))...);
363 }
364 
365 // Function: emplace
366 template <typename C>
368 
369  // dynamic tasking
370  if constexpr(std::is_invocable_v<C, Subflow&>) {
371  auto n = _graph.emplace_back(std::in_place_type_t<Node::DynamicWork>{},
372  [c=std::forward<C>(c)] (Subflow& fb) mutable {
373  // first time execution
374  if(fb._graph.empty()) {
375  c(fb);
376  }
377  });
378  return Task(n);
379  }
380  // condition tasking
381  else if constexpr(std::is_same_v<typename function_traits<C>::return_type, int>) {
382  auto n = _graph.emplace_back(
383  std::in_place_type_t<Node::ConditionWork>{}, std::forward<C>(c)
384  );
385  return Task(n);
386  }
387  // static tasking
388  else if constexpr(std::is_same_v<typename function_traits<C>::return_type, void>) {
389  auto n = _graph.emplace_back(
390  std::in_place_type_t<Node::StaticWork>{}, std::forward<C>(c)
391  );
392  return Task(n);
393  }
394  // placeholder
395  else if constexpr(std::is_same_v<C, std::monostate>) {
396  auto n = _graph.emplace_back();
397  return Task(n);
398  }
399  else {
400  static_assert(dependent_false_v<C>, "invalid task work type");
401  }
402 }
403 
404 // Function: composed_of
406  auto node = _graph.emplace_back();
407  node->_module = &taskflow;
408  return Task(node);
409 }
410 
411 // Procedure: precede
412 inline void FlowBuilder::precede(Task from, Task to) {
413  from._node->_precede(to._node);
414 }
415 
416 // Procedure: broadcast
418  for(auto to : tos) {
419  from.precede(to);
420  }
421 }
422 
423 // Procedure: broadcast
425  for(auto to : tos) {
426  from.precede(to);
427  }
428 }
429 
430 // Function: gather
431 inline void FlowBuilder::gather(std::vector<Task>& froms, Task to) {
432  for(auto from : froms) {
433  to.succeed(from);
434  }
435 }
436 
437 // Function: gather
439  for(auto from : froms) {
440  to.succeed(from);
441  }
442 }
443 
444 // Function: placeholder
446  auto node = _graph.emplace_back();
447  return Task(node);
448 }
449 
450 // Function: parallel_for
451 template <typename I, typename C>
453  I beg, I end, C&& c, size_t chunk
454 ){
455 
456  using category = typename std::iterator_traits<I>::iterator_category;
457 
458  auto S = placeholder();
459  auto T = placeholder();
460  //auto D = std::distance(beg, end);
461 
462  // default partition equals to the worker count
463  if(chunk == 0) {
464  chunk = 1;
465  }
466 
467  while(beg != end) {
468 
469  auto e = beg;
470 
471  // Case 1: random access iterator
472  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
473  size_t x = std::distance(beg, end);
474  std::advance(e, std::min(x, chunk));
475  }
476  // Case 2: non-random access iterator
477  else {
478  for(size_t i=0; i<chunk && e != end; ++e, ++i);
479  }
480 
481  // Create a task
482  auto task = emplace([beg, e, c] () mutable {
483  std::for_each(beg, e, c);
484  });
485 
486  S.precede(task);
487  task.precede(T);
488 
489  // adjust the pointer
490  beg = e;
491  }
492 
493  // special case
494  if(S.num_successors() == 0) {
495  S.precede(T);
496  }
497 
498  return std::make_pair(S, T);
499 
500 
501  /*using category = typename std::iterator_traits<I>::iterator_category;
502 
503  auto S = placeholder();
504  auto T = placeholder();
505  auto D = std::distance(beg, end);
506 
507  // special case
508  if(D == 0) {
509  S.precede(T);
510  return std::make_pair(S, T);
511  }
512 
513  // default partition equals to the worker count
514  if(p == 0) {
515  p = std::max(unsigned{1}, std::thread::hardware_concurrency());
516  }
517 
518  size_t b = (D + p - 1) / p; // block size
519  size_t r = (D % p) ? D % p : p; // workers to take b
520  size_t w = 0; // worker id
521 
522  while(beg != end) {
523 
524  auto e = beg;
525  size_t g = (w++ >= r) ? b - 1 : b;
526 
527  // Case 1: random access iterator
528  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
529  size_t x = std::distance(beg, end);
530  std::advance(e, std::min(x, g));
531  }
532  // Case 2: non-random access iterator
533  else {
534  for(size_t i=0; i<g && e != end; ++e, ++i);
535  }
536 
537  // Create a task
538  auto task = emplace([beg, e, c] () mutable {
539  std::for_each(beg, e, c);
540  });
541  S.precede(task);
542  task.precede(T);
543 
544  // adjust the pointer
545  beg = e;
546  }
547 
548  return std::make_pair(S, T); */
549 }
550 
551 // Function: parallel_for
552 template <
553  typename I,
554  typename C,
555  std::enable_if_t<std::is_arithmetic_v<I>, void>*
556 >
557 std::pair<Task, Task> FlowBuilder::parallel_for(I beg, I end, I s, C&& c, size_t chunk) {
558 
559  using T = std::decay_t<I>;
560 
561  if((s == 0) || (beg < end && s <= 0) || (beg > end && s >=0) ) {
562  TF_THROW(Error::TASKFLOW,
563  "invalid range [", beg, ", ", end, ") with step size ", s
564  );
565  }
566 
567  // source and target
568  auto source = placeholder();
569  auto target = placeholder();
570 
571  if(chunk == 0) {
572  chunk = 1;
573  }
574 
575  // Integer indices
576  if constexpr(std::is_integral_v<T>) {
577  // positive case
578  if(beg < end) {
579  while(beg != end) {
580  auto o = static_cast<T>(chunk) * s;
581  auto e = std::min(beg + o, end);
582  auto task = emplace([=] () mutable {
583  for(auto i=beg; i<e; i+=s) {
584  c(i);
585  }
586  });
587  source.precede(task);
588  task.precede(target);
589  beg = e;
590  }
591  }
592  // negative case
593  else if(beg > end) {
594  while(beg != end) {
595  auto o = static_cast<T>(chunk) * s;
596  auto e = std::max(beg + o, end);
597  auto task = emplace([=] () mutable {
598  for(auto i=beg; i>e; i+=s) {
599  c(i);
600  }
601  });
602  source.precede(task);
603  task.precede(target);
604  beg = e;
605  }
606  }
607  }
608  // We enumerate the entire sequence to avoid floating error
609  else if constexpr(std::is_floating_point_v<T>) {
610 
611  // positive case
612  if(beg < end) {
613  size_t N=0;
614  I b = beg;
615  for(I e=beg; e<end; e+=s) {
616  if(++N == chunk) {
617  auto task = emplace([=] () mutable {
618  for(size_t i=0; i<N; ++i, b+=s) {
619  c(b);
620  }
621  });
622  source.precede(task);
623  task.precede(target);
624  N = 0;
625  b = e;
626  }
627  }
628 
629  if(N) {
630  auto task = emplace([=] () mutable {
631  for(size_t i=0; i<N; ++i, b+=s) {
632  c(b);
633  }
634  });
635  source.precede(task);
636  task.precede(target);
637  }
638  }
639  else if(beg > end) {
640  size_t N=0;
641  I b = beg;
642  for(I e=beg; e>end; e+=s) {
643  if(++N == chunk) {
644  auto task = emplace([=] () mutable {
645  for(size_t i=0; i<N; ++i, b+=s) {
646  c(b);
647  }
648  });
649  source.precede(task);
650  task.precede(target);
651  N = 0;
652  b = e;
653  }
654  }
655 
656  if(N) {
657  auto task = emplace([=] () mutable {
658  for(size_t i=0; i<N; ++i, b+=s) {
659  c(b);
660  }
661  });
662  source.precede(task);
663  task.precede(target);
664  }
665  //while(beg > end) {
666  // size_t N = 0;
667  // auto e = beg;
668  // while(e > end && N < chunk) {
669  // e+=s;
670  // ++N;
671  // }
672  // auto task = emplace([=] () mutable {
673  // for(size_t i=0; i<N; ++i, beg+=s) {
674  // c(beg);
675  // }
676  // });
677  // source.precede(task);
678  // task.precede(target);
679  // beg = e;
680  //}
681  }
682  }
683 
684  if(source.num_successors() == 0) {
685  source.precede(target);
686  }
687 
688  return std::make_pair(source, target);
689 }
690 
691 // Function: reduce_min
692 // Find the minimum element over a range of items.
693 template <typename I, typename T>
695  return reduce(beg, end, result, [] (const auto& l, const auto& r) {
696  return std::min(l, r);
697  });
698 }
699 
700 // Function: reduce_max
701 // Find the maximum element over a range of items.
702 template <typename I, typename T>
704  return reduce(beg, end, result, [] (const auto& l, const auto& r) {
705  return std::max(l, r);
706  });
707 }
708 
709 // Function: transform_reduce
710 template <typename I, typename T, typename B, typename U>
712  I beg, I end, T& result, B&& bop, U&& uop
713 ) {
714 
715  using category = typename std::iterator_traits<I>::iterator_category;
716 
717  // Even partition
718  size_t d = std::distance(beg, end);
719  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
720  size_t g = std::max((d + w - 1) / w, size_t{2});
721 
722  auto source = placeholder();
723  auto target = placeholder();
724 
725  //std::vector<std::future<T>> futures;
726  auto g_results = std::make_unique<T[]>(w);
727  size_t id {0};
728 
729  while(beg != end) {
730 
731  auto e = beg;
732 
733  // Case 1: random access iterator
734  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
735  size_t r = std::distance(beg, end);
736  std::advance(e, std::min(r, g));
737  }
738  // Case 2: non-random access iterator
739  else {
740  for(size_t i=0; i<g && e != end; ++e, ++i);
741  }
742 
743  // Create a task
744  auto task = emplace([beg, e, bop, uop, res=&(g_results[id])] () mutable {
745  *res = uop(*beg);
746  for(++beg; beg != e; ++beg) {
747  *res = bop(std::move(*res), uop(*beg));
748  }
749  });
750 
751  source.precede(task);
752  task.precede(target);
753 
754  // adjust the pointer
755  beg = e;
756  id ++;
757  }
758 
759  // target synchronizer
760  target.work([&result, bop, res=MoC{std::move(g_results)}, w=id] () {
761  for(auto i=0u; i<w; i++) {
762  result = bop(std::move(result), res.object[i]);
763  }
764  });
765 
766  return std::make_pair(source, target);
767 }
768 
769 // Function: transform_reduce
770 template <typename I, typename T, typename B, typename P, typename U>
772  I beg, I end, T& result, B&& bop, P&& pop, U&& uop
773 ) {
774 
775  using category = typename std::iterator_traits<I>::iterator_category;
776 
777  // Even partition
778  size_t d = std::distance(beg, end);
779  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
780  size_t g = std::max((d + w - 1) / w, size_t{2});
781 
782  auto source = placeholder();
783  auto target = placeholder();
784 
785  auto g_results = std::make_unique<T[]>(w);
786 
787  size_t id {0};
788  while(beg != end) {
789 
790  auto e = beg;
791 
792  // Case 1: random access iterator
793  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
794  size_t r = std::distance(beg, end);
795  std::advance(e, std::min(r, g));
796  }
797  // Case 2: non-random access iterator
798  else {
799  for(size_t i=0; i<g && e != end; ++e, ++i);
800  }
801 
802  // Create a task
803  auto task = emplace([beg, e, uop, pop, res= &g_results[id]] () mutable {
804  *res = uop(*beg);
805  for(++beg; beg != e; ++beg) {
806  *res = pop(std::move(*res), *beg);
807  }
808  });
809  //auto [task, future] = emplace([beg, e, uop, pop] () mutable {
810  // auto init = uop(*beg);
811  // for(++beg; beg != e; ++beg) {
812  // init = pop(std::move(init), *beg);
813  // }
814  // return init;
815  //});
816  source.precede(task);
817  task.precede(target);
818  //futures.push_back(std::move(future));
819 
820  // adjust the pointer
821  beg = e;
822  id ++;
823  }
824 
825  // target synchronizer
826  target.work([&result, bop, g_results=MoC{std::move(g_results)}, w=id] () {
827  for(auto i=0u; i<w; i++) {
828  result = bop(std::move(result), std::move(g_results.object[i]));
829  }
830  });
831  //target.work([&result, futures=MoC{std::move(futures)}, bop] () {
832  // for(auto& fu : futures.object) {
833  // result = bop(std::move(result), fu.get());
834  // }
835  //});
836 
837  return std::make_pair(source, target);
838 }
839 
841 //template <typename I>
842 //size_t FlowBuilder::_estimate_chunk_size(I beg, I end, I step) {
843 //
844 // using T = std::decay_t<I>;
845 //
846 // size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
847 // size_t N = 0;
848 //
849 // if constexpr(std::is_integral_v<T>) {
850 // if(beg <= end) {
851 // N = (end - beg + step - 1) / step;
852 // }
853 // else {
854 // N = (end - beg + step + 1) / step;
855 // }
856 // }
857 // else if constexpr(std::is_floating_point_v<T>) {
858 // N = static_cast<size_t>(std::ceil((end - beg) / step));
859 // }
860 // else {
861 // static_assert(dependent_false_v<T>, "can't deduce chunk size");
862 // }
863 //
864 // return (N + w - 1) / w;
865 //}
866 
867 
868 // Procedure: _linearize
869 template <typename L>
870 void FlowBuilder::_linearize(L& keys) {
871 
872  auto itr = keys.begin();
873  auto end = keys.end();
874 
875  if(itr == end) {
876  return;
877  }
878 
879  auto nxt = itr;
880 
881  for(++nxt; nxt != end; ++nxt, ++itr) {
882  itr->_node->_precede(nxt->_node);
883  }
884 }
885 
886 // Procedure: linearize
888  _linearize(keys);
889 }
890 
891 // Procedure: linearize
893  _linearize(keys);
894 }
895 
896 // Proceduer: reduce
897 template <typename I, typename T, typename B>
898 std::pair<Task, Task> FlowBuilder::reduce(I beg, I end, T& result, B&& op) {
899 
900  using category = typename std::iterator_traits<I>::iterator_category;
901 
902  size_t d = std::distance(beg, end);
903  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
904  size_t g = std::max((d + w - 1) / w, size_t{2});
905 
906  auto source = placeholder();
907  auto target = placeholder();
908 
909  //T* g_results = static_cast<T*>(malloc(sizeof(T)*w));
910  auto g_results = std::make_unique<T[]>(w);
911  //std::vector<std::future<T>> futures;
912 
913  size_t id {0};
914  while(beg != end) {
915 
916  auto e = beg;
917 
918  // Case 1: random access iterator
919  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
920  size_t r = std::distance(beg, end);
921  std::advance(e, std::min(r, g));
922  }
923  // Case 2: non-random access iterator
924  else {
925  for(size_t i=0; i<g && e != end; ++e, ++i);
926  }
927 
928  // Create a task
929  //auto [task, future] = emplace([beg, e, op] () mutable {
930  auto task = emplace([beg, e, op, res = &g_results[id]] () mutable {
931  *res = *beg;
932  for(++beg; beg != e; ++beg) {
933  *res = op(std::move(*res), *beg);
934  }
935  //auto init = *beg;
936  //for(++beg; beg != e; ++beg) {
937  // init = op(std::move(init), *beg);
938  //}
939  //return init;
940  });
941  source.precede(task);
942  task.precede(target);
943  //futures.push_back(std::move(future));
944 
945  // adjust the pointer
946  beg = e;
947  id ++;
948  }
949 
950  // target synchronizer
951  //target.work([&result, futures=MoC{std::move(futures)}, op] () {
952  // for(auto& fu : futures.object) {
953  // result = op(std::move(result), fu.get());
954  // }
955  //});
956  target.work([g_results=MoC{std::move(g_results)}, &result, op, w=id] () {
957  for(auto i=0u; i<w; i++) {
958  result = op(std::move(result), g_results.object[i]);
959  }
960  });
961 
962  return std::make_pair(source, target);
963 }
964 
965 // ----------------------------------------------------------------------------
966 // Cyclic Dependency: Task
967 // ----------------------------------------------------------------------------
968 
969 // Function: work
970 template <typename C>
971 Task& Task::work(C&& c) {
972 
973  if(_node->_module) {
974  TF_THROW(Error::TASKFLOW, "can't assign work to a module task");
975  }
976 
977  // static tasking
978  if constexpr(std::is_same_v<typename function_traits<C>::return_type, void>) {
979  _node->_work.emplace<Node::StaticWork>(std::forward<C>(c));
980  }
981  // condition tasking
982  else if constexpr(std::is_same_v<typename function_traits<C>::return_type, int>) {
983  _node->_work.emplace<Node::ConditionWork>(std::forward<C>(c));
984  }
985  // dyanmic tasking
986  else if constexpr(std::is_invocable_v<C, Subflow&>) {
987  _node->_work.emplace<Node::DynamicWork>(
988  [c=std::forward<C>(c)] (Subflow& fb) mutable {
989  // first time execution
990  if(fb._graph.empty()) {
991  c(fb);
992  }
993  });
994  }
995  // placeholder
996  else if constexpr(std::is_same_v<C, std::monostate>) {
997  _node->_work.emplace<std::monostate>();
998  }
999  else {
1000  static_assert(dependent_false_v<C>, "invalid task work type");
1001  }
1002 
1003  return *this;
1004 }
1005 
1006 // ----------------------------------------------------------------------------
1007 // Legacy code
1008 // ----------------------------------------------------------------------------
1009 
1010 
1011 using SubflowBuilder = Subflow;
1012 
1013 } // end of namespace tf. ---------------------------------------------------
1014 
1015 
void linearize(std::vector< Task > &tasks)
adds adjacent dependency links to a linear list of tasks
Definition: flow_builder.hpp:887
Task emplace(C &&callable)
creates a task from a given callable object
Definition: flow_builder.hpp:367
void broadcast(Task A, std::vector< Task > &others)
adds dependency links from one task A to many tasks
Definition: flow_builder.hpp:417
std::pair< Task, Task > transform_reduce(I beg, I end, T &result, B &&bop, U &&uop)
constructs a task dependency graph of parallel transformation and reduction
Definition: flow_builder.hpp:711
Definition: taskflow.hpp:5
T hardware_concurrency(T... args)
Task placeholder()
creates an empty task
Definition: flow_builder.hpp:445
Subflow(Args &&... args)
constructs a subflow builder object
Definition: flow_builder.hpp:331
void detach()
enables the subflow to detach from its parent task
Definition: flow_builder.hpp:341
bool detached() const
queries if the subflow will be detached from its parent task
Definition: flow_builder.hpp:346
std::pair< Task, Task > reduce_max(I beg, I end, T &result)
constructs a task dependency graph of parallel reduction through std::max
Definition: flow_builder.hpp:703
Task & succeed(Ts &&... tasks)
adds precedence links from other tasks to this
Definition: task.hpp:197
std::pair< Task, Task > parallel_for(I beg, I end, C &&callable, size_t chunk=1)
constructs a task dependency graph of range-based parallel_for
Definition: flow_builder.hpp:452
Task composed_of(Taskflow &taskflow)
creates a module task from a taskflow
Definition: flow_builder.hpp:405
void precede(Task A, Task B)
adds a dependency link from task A to task B
Definition: flow_builder.hpp:412
the class to create a task dependency graph
Definition: core/taskflow.hpp:18
void gather(std::vector< Task > &others, Task A)
adds dependency links from many tasks to one task A
Definition: flow_builder.hpp:431
FlowBuilder(Graph &graph)
construct a flow builder object
Definition: flow_builder.hpp:283
Building blocks of a task dependency graph.
Definition: flow_builder.hpp:13
bool joined() const
queries if the subflow will join its parent task
Definition: flow_builder.hpp:351
task handle to a node in a task dependency graph
Definition: task.hpp:22
Task & precede(Ts &&... tasks)
adds precedence links from this to other tasks
Definition: task.hpp:190
std::pair< Task, Task > reduce(I beg, I end, T &result, B &&bop)
construct a task dependency graph of parallel reduction
Definition: flow_builder.hpp:898
Task & work(C &&callable)
assigns a new callable object to the task
Definition: flow_builder.hpp:971
The building blocks of dynamic tasking.
Definition: flow_builder.hpp:294
void join()
enables the subflow to join its parent task
Definition: flow_builder.hpp:336
std::pair< Task, Task > reduce_min(I beg, I end, T &result)
constructs a task dependency graph of parallel reduction through std::min
Definition: flow_builder.hpp:694