Using OpenMP to easily parallelize applications is attractive because of its programmability. Nonetheless, knowing the internals of the language may not be as easy as expected. Furthermore, debugging parallel programs to find errors at run-time can be arduous. In this context, the compiler is a key tool to help programmers finding correctness errors.
The presented algorithms are based on classic techniques of control and data-flow analysis. They include OpenMP support to detect correctness mistakes related with synchronizations, data-sharing attributes and dependence clauses associated with tasks. We classify these mistakes in different case studies and propose solutions to fix them. We also implement all the algorithms in the Mercurium source-to-source compiler, which we use to test the usefulness of the proposal with a number of end-users and benchmarks. On one hand we test our tool with several students, gathering logs of their executions to study the more common errors. On the other hand, we compare our results with those of Oracle Solaris Studio which, to the best of our knowledge, is the only compiler implementing such a correctness checking feature.
Based on our tests, OpenMP beginner programmers often make mistakes related to the data-sharing attributes and the dependence clauses. These mistakes are mainly related with the default data-sharing attributes, e.g., programmers forget to explicitly determine as shared variables
which are firstprivate by default, or they forgot the explicitly determine as private variables which are firstprivate by default. The first case leads to a wrong result of the program, whereas the second leads to a loss of performance due to an unnecessary copy. The other most common mistake is to define a program with race conditions as a result of a wrong synchronization of either the tasks or the access to the variables. This mistake leads to non-deterministic results.
According to our comparison with Oracle Solaris Studio, the algorithms implemented in Mercurium cover more cases and propose more accurate hints. While Mercurium addresses both correctness and performance issues, Studio only tackles correctness. Messages related with the variables involved in a race condition are more accurate in the Studio compiler though in the sense that they point out which accesses are in a race.
Even experienced programmers can make mistakes very hard to find at run-time, especially in large codes. This is why a compile-time tool providing correctness tips is always useful and effortless from the programmer point of view.
5
A Static Task Dependency Graph for
OpenMP
OpenMP is increasingly being adopted by modern many-core embedded processors to exploit their parallel computation capabilities. Unfortunately, current OpenMP runtime libraries are not suitable for processors relying on small and fast on-chip memories, due to its memory consumption. In this part of the thesis we present a complete tool-chain that enables the execution of codes based on the OpenMP tasking model on such systems. The tool-chain is based on a compiler transformation that is able to statically generate a TDG, and the runtime support to execute this TDG instead of the regular mechanism for dynamic checking. The reduction in memory consumption is accomplished as a result of the efficiency of the mechanism used to store and access the TDG.
5.1
Applicability
Although OpenMP was originally focused on massively data-parallel loop-intensive applications, the latest specifications have evolved to support dynamic and irregular parallelism, as well as heterogeneity. By virtue of these extension, the language has gained much attention in the real-time embedded domain [32,159]. Furthermore, real-time applications are usually modeled as a Directed Acyclic Graph (DAG) to analyze its timing and functional properties, and the OpenMP tasking model can be represented as a TDG [155], a type of DAG. For that reason, the tasking model is suitable to exploit the capabilities of current many-core embedded platforms (e.g., Kalray MPPA [41], STM P2012 [28], TI Keystone II [144]), and thus deliver the level of performance required to face current and future challenges of embedded systems.
Current implementations of OpenMP (e.g., libgomp [60], Nanos++[15]) generate the TDG at run-time for two reasons: 1) the TDG depends on the tasks that are instantiated, which is determined by the CFG, and 2) the addresses of the data elements upon which dependences are built are known at run-time. Consequently, large data structures are required to manage the tasking model, as shown in Table 5.1 (The size of the structure that holds a task depends on the variables used within the task and the dependencies the task may have with other tasks. Nanox uses significantly more memory because it supports a more complex model to that of OpenMP, as
Runtime library libgomp Nanox++
GCC 5.4 GCC 7.1
Size(Bytes) 176 208 1056
Table 5.1: Minimum memory (in Bytes) used to store an OpenMP task in different runtimes. introduced in Section 2.1.2). Modern many-core embedded designs, however, rely on computing platforms with small on-chip memories that are accessible by a limited number of cores (usually organized in clusters), making these runtimes unsuitable.
As an illustration, we examine the memory consumption of the libgomp runtime library. In this implementation, when a new task is created, its in and out dependences are matched against those of the existing tasks. To do so, each task region maintains a hash table that stores the memory address of each data element defined in the out and inout clauses, and the list of tasks associated with the task it represents. Each position of the hash table is linked to those tasks depending on the object it represents. The runtime can quickly identify which successors may be ready for execution when the task completes by accessing its hash table. This table is cleared when a task reaches a taskwait or a barrier, when all pending tasks must be resolved. Removing the information of a single task at completion may turn out to be very costly, because dependent tasks are tracked in multiple linked lists. As a result, memory consumption may significantly increase as the number of concurrently instantiated tasks increases.
We prove that storing a complete statically generated TDG can result in a huge reduction of the memory used at run-time. Although this idea may seem counter-intuitive, the data structures needed to store a static TDG are much lighter than those necessary to dynamically build the TDG. Moreover, statically deriving the TDG provides an extra benefit: it allows applying real-time DAG scheduling models [24], from which timing guarantees can be derived [96,139].
5.2
Related work
Sarkar et al. [136] presented a framework for partitioning and scheduling tasks at compile-time balancing tasks granularity, and thus overhead and parallelism. Vijaykumar et al. [157] proposed a set of heuristics to generate a TDG for massively data-parallel applications based on the CFG and use-definition analysis, aiming at reducing communication and synchronization overheads. Yet none of these methodologies are able to create at compile-time a TDG for complex and irregular algorithms.
Pugh et al. [30] proposed a new technique to detect dependencies at compile-time and map HPC kernels into cluster nodes. The disadvantage is that it is expensive and introduces overhead to the compiler while, in our proposal, much simpler dependency analyses are enough to check tasks parallelism.
Tzenakis et al. [152] implemented task instantiation, dependence analysis and scheduling techniques, and proved their efficiency over runtimes such as SMPSs [13]. However, this method has runtime overhead and requires heavy data-structures for dynamic dependency checking.
Finally, Arandi et al. [10] presented a hybrid approach to try to get the best of both static and dynamic methods, but the technique still introduces too much overhead, as the authors admit.
Furthermore, Liu et al. [89] proposed an OpenMP runtime for multi-core platforms with limited memory resources. However, this runtime only implements OpenMP 2.5, in which the tasking model is not supported.