{"id":33823,"date":"2024-04-30T18:00:01","date_gmt":"2024-04-30T18:00:01","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cppblog\/?p=33823"},"modified":"2024-05-01T06:40:02","modified_gmt":"2024-05-01T06:40:02","slug":"openmp-improvements-in-visual-studio-c-c-compiler-triangular-loop-collapse","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/cppblog\/openmp-improvements-in-visual-studio-c-c-compiler-triangular-loop-collapse\/","title":{"rendered":"OpenMP improvements in Visual Studio C\/C++ compiler: triangular loop collapse"},"content":{"rendered":"<p><span data-contrast=\"auto\">Our <a href=\"https:\/\/devblogs.microsoft.com\/cppblog\/open-mp-improvements-in-visual-studio-cpp\">previous blog post<\/a> about OpenMP support in Visual Studio announced support for the loop <code>collapse<\/code> feature in version 17.8.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">In the meantime, we have continued working on improving the feature and, in Visual Studio 2022 17.10, we have added a new, more efficient algorithm for partitioning work of certain types of collapsed triangular loops supported by the OpenMP Standard 5.2. All this work continues to be accessible if you are using the <code>-openmp:llvm<\/code> switch (see the <a href=\"https:\/\/devblogs.microsoft.com\/cppblog\/improved-openmp-support-for-cpp-in-visual-studio\/\">Improved OpenMP Support for C++ in Visual Studio<\/a> blogpost for details about this switch). In this blog we will describe this algorithm in some detail:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<h2><span data-contrast=\"auto\">Triangular loops in OpenMP 5.2<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/h2>\n<p><span data-contrast=\"auto\">In the most recent OpenMP Standard 5.2, inner loops&#8217; lower and upper bounds can depend on outer loops&#8217; induction variables, making the loop space non-rectangular. For example, a double nested loop case like this:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">void bar(float *a, int i, int j);\u00a0\r\nvoid foo(float *a)\u00a0\r\n{\u00a0\r\n\u00a0 #pragma omp parallel for collapse(2)\u00a0\r\n\u00a0 for (int i = 0; i &lt; M; i++)\u00a0\r\n\u00a0\u00a0\u00a0 for (int j = i; j &lt; M; j++)\u00a0\u00a0\r\n\u00a0\u00a0\u00a0\u00a0\u00a0 bar(a, i, j);\u00a0\r\n}\u00a0<\/code><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/pre>\n<p><span data-contrast=\"none\">Since a triangular loop space makes only one half of the corresponding rectangular loop space, using the rectangular space partitioning algorithm for distributing the work would be functionally correct, but only 50% efficient<\/span><span data-contrast=\"auto\">. To approach 100% efficiency, we have added a separate algorithm to handle collapsed triangular loops which considers only the applicable half of the rectangular space. Our current implementation covers only certain types of triangular loops, specifically double nested lower (both excluding and including the diagonal) and upper equilateral triangular one-increment loops described below. However, this could be extended to cover a wider range of triangular loop types.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">We begin by identifying a supported nested loop case by calling a new function <code>kmp_identify_nested_loop_structure<\/code> from <code>__kmpc_for_collapsed_init<\/code>.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">\/**************************************************************************\u00a0\r\n\u00a0* Identify nested loop structure - loops come in the canonical form\u00a0\r\n\u00a0* Lower triangle matrix: i = 0; i &lt;= M; i++\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 {0,0}:{M,0}\u00a0\r\n\u00a0*\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 j = 0; j &lt;= 0\/-1+1*i; j++ {0,0}:{0\/-1,1}\u00a0\r\n\u00a0* Upper Triangle matrix\u00a0\r\n\u00a0*\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 i = 0;\u00a0\u00a0\u00a0\u00a0 i &lt;= M; i++\u00a0\u00a0\u00a0 {0,0}:{M,0}\u00a0\r\n\u00a0*\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 j = 0+1*i; j &lt;= M; j++\u00a0\u00a0\u00a0 {0,1}:{M,0}\u00a0\r\n\u00a0* ************************************************************************\/\u00a0\r\nnested_loop_type_t\u00a0\r\nkmp_identify_nested_loop_structure(\/*in*\/ bounds_info_t *original_bounds_nest,\u00a0\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/*in*\/ kmp_index_t n) <\/code><\/pre>\n<p><span data-contrast=\"auto\">If the loop in question is of a supported type, we call the corresponding function, i.e. <code>kmp_handle_lower_triangle_matrix<\/code> or <code>kmp_handle_upper_triangle_matrix<\/code>, to handle work partitioning, with each of these functions returning the chunk assigned to the current thread for execution.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">We first describe the algorithm that handles the case of a lower triangular loop that excludes the diagonal, i.e.:<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">#pragma omp parallel for collapse(2)\u00a0\r\nfor (int i = 0; i &lt; M; i++)\u00a0\r\n    for (int j = 0; j &lt; i; j++) {\u00a0\r\n\u00a0\u00a0\u00a0\u00a0    \/\/...\u00a0\r\n\u00a0\u00a0\u00a0 } <\/code><\/pre>\n<p><span data-contrast=\"auto\">For this case, the total number of iterations in the combined loop space will be <code>T=M*(M-1)\/2<\/code>, and the most efficient work distribution between <code>N<\/code> threads will be for each of them to receive a chunk of <code>T\/N<\/code> iterations, with the first <code>T%N<\/code> threads getting one iteration extra. We assign chunks of combined space iterations to threads sequentially. The chunk for thread <code>n<\/code> begins at the (0-based) iteration number equal to the number of iterations executed by all the previous threads <code>A=n*T\/N<\/code> and ends at <code>A+T\/N<\/code>. <\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">We then translate <code>A<\/code> into the <code>{Is,Js}<\/code> tuple describing the chunk\u2019s start iteration. <code>Is<\/code> is the biggest such outer loop iteration that satisfies the condition <code>(Is-1)*Is\/2 &lt;= A =&gt; Is^2-Is-2A &lt;= 0<\/code>. We then solve <code>Is^2-Is-2A = 0<\/code> for <code>Is<\/code>, i.e., <code>Is = (sqrt(1+8*A)-1)\/2<\/code>. <code>Js<\/code> is then the remaining number of inner loop iterations required to reach <code>A<\/code>, i.e. <code>Js =A-Is*(Is-1)<\/code>. We then apply the same approach to translate <code>A+T\/N<\/code> into the <code>{Ie,Je}<\/code> tuple describing the chunk\u2019s end iteration. This tuple pair represents the work assigned to thread <code>n<\/code> and is given back to it for execution.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">To avoid the use of floating-point arithmetic, we use an additional function <code>sqrt_newton_approx<\/code> to calculate <code>sqrt<\/code> using the Newton method. The method is iterative, and since the chunk calculation rounds the result down to the nearest integer, we can limit its precision to 0.1 to reduce the number of iterations. The method converges quickly, with less than 10 iterations to achieve the result for M values below 10^3, and less than 20 iterations for M values below 10^9.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">The use of rounding in calculating chunk boundaries may introduce the risk of missing or duplicate iterations when dividing thread work. However, our implementation does not carry such risk since the same algorithm with the same inputs is used to calculate both the end iteration of thread <code>n<\/code> chunk and the start iteration of thread <code>n+1<\/code><\/span><span data-contrast=\"auto\">\u00a0chunk. This ensures the absence of gaps and overlaps between threads.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">We now consider a lower triangle loop that includes the diagonal, i.e.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">#pragma omp parallel for collapse(2)\u00a0\r\nfor (int i = 0; i &lt; M; i++)\u00a0\r\n \u00a0\u00a0 for (int j = 0; j &lt;= i; j++) {\u00a0\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/...\u00a0\r\n\u00a0\u00a0\u00a0 }<\/code><\/pre>\n<p><span data-contrast=\"auto\">For this case we can employ the same approach as above, with the main difference that <code>T=M*(M+1)\/2<\/code> instead of <code>T=M*(M-1)\/2<\/code> and the subsequent <code>Is<\/code> conditions are similarly <code>(Is+1)*Is<\/code> rather than <code>(Is-1)*Is<\/code>. <code>kmp_handle_lower_triangle_matrix<\/code>\u00a0handles this simple difference by parameterizing calculations based on the loop description.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">We now consider an upper triangular loop, i.e.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">#pragma omp parallel for collapse(2)\u00a0\r\nfor (int i = 0; i &lt; M; i++)\u00a0\r\n\u00a0\u00a0\u00a0 for (int j = i; j &lt; M; j++) {\u00a0\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/...\u00a0\r\n\u00a0\u00a0\u00a0 }\u00a0<\/code><\/pre>\n<p><span data-contrast=\"auto\">This loop is a flipped case of the lower triangular loop including the diagonal. <code>kmp_handle_upper_triangle_matrix<\/code> thus follows the same algorithm as <code>kmp_handle_lower_triangle_matrix<\/code>\u00a0for the lower triangular loop that includes the diagonal and then simply flips the result back to the upper triangular case.\u00a0 With some further parameterization, these two functions could be combined into one, but we decided to keep them separate for code clarity.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<p><span data-contrast=\"auto\">While the performance benefits of the improved handling for these triangular loop types will always depend on the nature and size of the underlying workload, including its micro-architectural behavior, our tests show that the new code achieves the perfect distribution of the supported triangular loops workload between threads, giving the expected up to 2x performance benefit on simulated perfectly scaling workloads.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n<h2><span data-contrast=\"auto\">Feedback<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/h2>\n<p><span data-contrast=\"auto\">We encourage you to try this new code in Visual Studio 2022 version 17.10 or newer. It has also been up-streamed into LLVM (https:\/\/github.com\/llvm\/llvm-project\/pull\/83939). As always, we welcome your feedback which we could use to continue improving the loop collapse feature. Please share your thoughts, comments and questions with us through <a href=\"https:\/\/developercommunity.visualstudio.com\/home\">Developer Community<\/a>. You can also reach us on X <a href=\"https:\/\/twitter.com\/visualc\">@VisualC<\/a>, or via email at visualcpp@microsoft.com.<\/span><span data-ccp-props=\"{&quot;201341983&quot;:0,&quot;335559739&quot;:0,&quot;335559740&quot;:240}\">\u00a0<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Our previous blog post about OpenMP support in Visual Studio announced support for the loop collapse feature in version 17.8.\u00a0 In the meantime, we have continued working on improving the feature and, in Visual Studio 2022 17.10, we have added a new, more efficient algorithm for partitioning work of certain types of collapsed triangular loops [&hellip;]<\/p>\n","protected":false},"author":153229,"featured_media":35994,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,256,3921],"tags":[],"class_list":["post-33823","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cplusplus","category-experimental","category-openmp"],"acf":[],"blog_post_summary":"<p>Our previous blog post about OpenMP support in Visual Studio announced support for the loop collapse feature in version 17.8.\u00a0 In the meantime, we have continued working on improving the feature and, in Visual Studio 2022 17.10, we have added a new, more efficient algorithm for partitioning work of certain types of collapsed triangular loops [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts\/33823","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/users\/153229"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/comments?post=33823"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts\/33823\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/media\/35994"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/media?parent=33823"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/categories?post=33823"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/tags?post=33823"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}