使用英特尔 OpenMP 找到最佳线程数:只有 1 个线程比许多线程有更好的结果
Posted
技术标签:
【中文标题】使用英特尔 OpenMP 找到最佳线程数:只有 1 个线程比许多线程有更好的结果【英文标题】:Find the best number of thread with Intel OpenMP : only 1 thread has better results than many threads 【发布时间】:2020-05-10 09:02:35 【问题描述】:在我的代码中多次使用以下类型的循环:
#pragma omp parallel for schedule(dynamic, num_threads)
for(int i=0; i<F_matrix_A.size(); i++)
for(int j=0; j<F_matrix_A.size(); j++)
F_previous_T[i][j] = F_previous[j][i];
#pragma omp parallel for schedule(dynamic, num_threads)
for(int i=0; i<F_matrix_A.size(); i++)
for(int k=0; k<F_matrix_A.size(); k++)
for(int j=0; j<=i; j++)
if(F_previous_T[i][k] != 0 && F_previous[k][j] !=0)
Fisher_new[i][j] += F_previous_T[i][k]*F_previous[k][j];
当我在参数之前设置时,我得到了最好的性能:#define num_threads 1
我在一个 64 核的工作站上工作(当我执行 /proc/cpuinfo
时,我看到了 128 个处理器)。我觉得不能从这么多的进程中受益是很遗憾的。
是不是因为我使用了特定的编译指示:
#pragma omp parallel for schedule(dynamic, num_threads)
??
还有其他方法可以缩短运行时间吗?我在不同的论坛上看到,使用大量进程可能会导致大量开销。
我的循环大小通常为 1700x1700。
如果有人有想法,就说出来就好了。
更新 1:我的代码有 2 个版本,一个带有 GNU g++
,另一个带有 Intel icpc
1) 我正在使用 Makefile 后面的“通用”:
ifneq "$(MAKECMDGOALS)" "clean"
include $(MAKECMDGOALS).make
endif
OBJECTS = $(SOURCES:.cpp=.o)
$(MAKECMDGOALS): $(SOURCES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CXX) $(LDFLAGS) $(OBJECTS) -o $@
.cpp.o:
$(CXX) $(CXXFLAGS) $(LDFLAGS) $< -o $@
clean:
rm -f *.o
1) 对于GNU g++
,我使用gnu.make
文件编译:
CXX = g++ -std=c++11 -O3 -fopenmp
CXXFLAGS = -Wall -c
LDFLAGS = -march=native
LDFLAGS =
SOURCES = main.cpp TSAF_gnu.cpp
EXECUTABLE = main_gnu.exe
2) 对于Intel icpc
,我使用intel.make
文件编译:
CXX = icpc -std=c++11 -O3 -xHost -qopenmp
CXXFLAGS = -Wall -c -I$MKLROOT/include
LDFLAGS = -mkl=parallel
LDFLAGS += -L$MKLROOT/lib/intel64_lin -Wl,-rpath,$MKLROOT/lib/intel64_lin -lmkl_intel_lp64 -lmkl_intel_thread \
-lmkl_core -liomp5 -lpthread
SOURCES = main.cpp TSAF_intel.cpp
EXECUTABLE = main_intel.exe
标准运行大约需要 3 分钟。
【问题讨论】:
什么是编译标志?您使用哪个优化级别?此外,这种大小的矩阵对于并行处理来说可能太小了。单线程运行需要多长时间? @DanielLangr 我添加了一个 UPDATE 1 为您提供更多信息。 一目了然:(1) 你几乎不能编写对缓存不友好的访问模式,遍历一个数组行优先和另一个列优先。 (2) 每个循环的“计算负载”是微不足道的。 (3)if
隐藏在深层循环嵌套底部的语句是一种以非常高的速率混淆分支预测的好方法。 (4) schedule(dynamic, *small_chunk_size*)
可能是解决问题的最糟糕的时间表。现在,对于您的应用程序来说,这些都可能无法避免,但您所写的几乎是一个教科书示例,说明何时不 使用 OpenMP。
您如何摆脱最内层循环中的if (...)
并废弃schedule
子句?如果两个因子中的任何一个为零,则乘积无论如何都将为零,并且乘法非常快。此外,schedule(dynamic)
有 HUGE 开销。
【参考方案1】:
schedule(dynamic, num_threads)
行可能会导致可伸缩性问题。
确实,对于大小为 1700 和 64 个线程的矩阵,动态调度策略的块大小为 64。因此,块的数量为 floor(1700/64) = 26
,这对于 64 个线程来说太小了!
即使有 32 个线程,工作平衡也不是很好。我认为每个线程至少有 3-4 个块很重要。
随着线程数增加粒度很奇怪。根据输入大小设置粒度可能更相关。我建议使用schedule(guided)
或schedule(dynamic,chunksize)
并将chunksize 设置为max(F_matrix_A.size() / (num_threads * 4), 1)
之类的东西(尽管如果不添加collapse
,使用schedule(dynamic,1)
应该不会那么糟糕)。
或者,您可以使用 task 和 taskloops 指令。
另外请注意,如果您在具有多个 NUMA 节点的机器上工作(这可能是因为有 64 个内核),您应该非常小心使用 动态 调度,因为线程可能会访问远程 NUMA 内存节点,这会显着降低性能(这显然是您在内存绑定代码中不想要的)。
更新:您可以同时在数组的两个垂直侧工作,以显着减少内循环计算时间的可变性。结果会是这样的:
#pragma omp parallel for schedule(static)
for(int i=0; i<(F_matrix_A.size()+1)/2; i++)
// Upper-part
for(int k=0; k<F_matrix_A.size(); k++)
for(int j=0; j<=i; j++)
if(F_previous_T[i][k] != 0 && F_previous[k][j] != 0)
Fisher_new[i][j] += F_previous_T[i][k]*F_previous[k][j];
// Lower-part (do not perform the middle twice)
if(i < F_matrix_A.size()/2)
const int i2 = F_matrix_A.size() - 1 - i;
for(int k=0; k<F_matrix_A.size(); k++)
for(int j=0; j<=i2; j++)
if(F_previous_T[i2][k] != 0 && F_previous[k][j] != 0)
Fisher_new[i2][j] += F_previous_T[i2][k]*F_previous[k][j];
【讨论】:
collapse(2)
当内部循环包含诸如Fisher_new[i][j] += ...
之类的语句并且j
循环在所有线程中独立运行时,确实是一个糟糕的选择。无法保证两个线程最终不会具有相同的 i
值。相反,很可能是这种情况,因为您建议的块大小是矩阵大小的一小部分。
我同意。它可能会导致数据竞争。我认为可以通过减少数组来执行collapse(2)
,但这可能效率低下。所以,我删除了这部分。谢谢。以上是关于使用英特尔 OpenMP 找到最佳线程数:只有 1 个线程比许多线程有更好的结果的主要内容,如果未能解决你的问题,请参考以下文章