[转]英特尔工具使在多个处理器上实现线程化更加轻松
作为使用多核处理器的软件开发人员,您将面临以下挑战:确定线程化技术是否有助于提高性能、是否值得投入精力、或者是否可以实现。
支持 OpenMP* 的英特尔® 编译器和线程工具(英特尔® 线程档案器和英特尔® 线程检查器)可以帮助您快速评估运行在两个、四个甚至多个处理器上的线程化应用的性能,并具体确定那些用于支持线程化且需要保护的数据在代码中的位置。所有这些评估都可以利用直观的、由编译器支持的 OpenMP 编译指示(pragma)在代码中执行。
采用这些工具,可以在单线程模式下运行代码,并可无需预先真正实现线程化代码,即可评估代码在实际的多核系统或多处理器系统中的运行情况。结合使用 OpenMP 与 英特尔® 线程档案器和英特尔® 线程检查器的这一评估方法称作"线程数独立模式",这一快速而强大的技术有助于评估线程性能并实现平衡。
此外,并行代码的开发可以在笔记本电脑或其它计算机系统上执行,这些系统虽然比目标系统的内核少,但仍可获得有关应用于这些系统的多核处理器的可扩展性评估。本文将讨论如何使用这些工具执行该分析。
采用英特尔® 线程档案器和英特尔® 线程检查器的线程数独立模式
对使用 "/Qopenmp /Qtcheck" 选项编译的程序进行分析时需采用英特尔® 线程检查器的线程数独立模式,然后采用英特尔® 线程检查器分析。对于线程数独立模式,很重要的一点就是,程序可能不会显式地控制(或依赖)操作的线程数。使用英特尔® 线程档案器的线程数独立模式有点复杂,在多处理器或多核系统上进行开发时,必须将 OpenMP* 线程数限制为 1 才可进行正确操作。以下部分将对此进行详细说明。
对于具有 /Qopenmp /Qtcheck 编译器选项的线程检查器,代码将采用串行模式,通过 OpenMP 并行编译指示自动运行,确定潜在的数据冲突,就好像程序正在并行中运行一样。这就是说在模拟的并行运行中不会出现实际的数据竞跑、死锁或其它的数据并行问题,但可以与在程序并行运行条件下一样对这些情况进行检测和报告。通常情况下,该方法适用于 parallel for 和其它数据分解编译指示以及功能分解并行段编译指示,但不适用于 taskq 或嵌套式并行处理。有关这些编译指示的详细信息,请参阅英特尔® 编译器所附带的 OpenMP 文档。即使程序正在以串行模式运行,也可以在无需将 OMP_NUM_THREADS 环境变量设置为 1 的情况下,就实现英特尔® 线程检查器对线程数独立模式的应用。事实上,通过观察代码是否仅在并行计算机的单个线程或内核上运行,就可以验证是否已触发线程数独立模式。
在线程数独立模式下将英特尔线程档案器与 /Qopenmp_profile 选项配合使用时,可以对采用 OpenMP 自动并行编译指示的代码进行模拟。虽然应用程序实际上以串行方式运行,但看起来代码正在并行运行,下面是几个重要的注意事项:您必须利用 omp_set_num_threads() 函数,通过配置对话框中的单 OMP 线程,在代码中显式地运行英特尔® 线程档案器。与英特尔® 线程检查器一样,简单的 OpenMP 编程结构将成为以线程数独立模式运行的最佳保障。特别是,线程数独立模式不支持 taskq 或嵌套式并行处理选项。调用诸如 omp_set_num_threads()、omp_get_num_threads()、omp_get_max_threads()、omp_get_thread_num() 和 omp_get_num_procs() 等函数时也可能会影响正在评估的代码,这样,代码将不再以线程数独立模式运行,这再次表明该代码以隐式方式依赖于指定的线程数。建议在此模式下简单地使用 OpenMP,因为它主要用于线程化可扩展性评估,而非实际的线程化。以下两部分对如何执行此分析进行了详细描述
对于具有 /Qopenmp /Qtcheck 编译器选项的线程检查器,代码将采用串行模式,通过 OpenMP 并行编译指示自动运行,确定潜在的数据冲突,就好像程序正在并行中运行一样。这就是说在模拟的并行运行中不会出现实际的数据竞跑、死锁或其它的数据并行问题,但可以与在程序并行运行条件下一样对这些情况进行检测和报告。通常情况下,该方法适用于 parallel for 和其它数据分解编译指示以及功能分解并行段编译指示,但不适用于 taskq 或嵌套式并行处理。有关这些编译指示的详细信息,请参阅英特尔® 编译器所附带的 OpenMP 文档。即使程序正在以串行模式运行,也可以在无需将 OMP_NUM_THREADS 环境变量设置为 1 的情况下,就实现英特尔® 线程检查器对线程数独立模式的应用。事实上,通过观察代码是否仅在并行计算机的单个线程或内核上运行,就可以验证是否已触发线程数独立模式。
在线程数独立模式下将英特尔线程档案器与 /Qopenmp_profile 选项配合使用时,可以对采用 OpenMP 自动并行编译指示的代码进行模拟。虽然应用程序实际上以串行方式运行,但看起来代码正在并行运行,下面是几个重要的注意事项:您必须利用 omp_set_num_threads() 函数,通过配置对话框中的单 OMP 线程,在代码中显式地运行英特尔® 线程档案器。与英特尔® 线程检查器一样,简单的 OpenMP 编程结构将成为以线程数独立模式运行的最佳保障。特别是,线程数独立模式不支持 taskq 或嵌套式并行处理选项。调用诸如 omp_set_num_threads()、omp_get_num_threads()、omp_get_max_threads()、omp_get_thread_num() 和 omp_get_num_procs() 等函数时也可能会影响正在评估的代码,这样,代码将不再以线程数独立模式运行,这再次表明该代码以隐式方式依赖于指定的线程数。建议在此模式下简单地使用 OpenMP,因为它主要用于线程化可扩展性评估,而非实际的线程化。以下两部分对如何执行此分析进行了详细描述
使用 OpenMP* 和英特尔® 线程档案器评估线程化应用的可扩展性和性能
在上述背景下,我们首先通过一个简单的实例来说明如何使用 OpenMP* 和英特尔® 线程档案器评估应用线程化的可扩展性。该方法的另一个强大功能就是,需要评估的代码不必真正进行线程化处理或达到线程安全要求;在真正开始线程化应用之前,可以使用该技术以简单、快速且有效的方法对不同的线程方法和模式进行评估。此方法实质上评估代码中的并行运行部分和串行运行部分,然后使用 Amdahl 定律计算代码以并行方式运行时潜在的可扩展性。再次注意,您无需实际并行运行代码。这一点可通过以下方法得到确定:在多处理器系统中的 "高级活动配置" 对话框中将线程数设置为 1,或在单逻辑处理器系统上进行代码评估。
如使用线程数独立模式支持线程档案器的构建,则必须使用英特尔® 编译器 8.0 或更高版本。首先,将 OpenMP 自动并行编译指示语句放在适当的位置,用以模拟代码中可能潜在并行运行的部分。简单地说,对于基本 OMP 编程而言,要在 for 环中支持数据分解,请使用 #pragma omp parallel for 语句。要支持功能分解,请使用 parallel section 语句。有关这些编程语句的信息及其它有关使用英特尔® 编译器的 OpenMP 编程的常规文档,请参阅编译器文档或本文结尾处的其它参考文章。接下来,使用 /fixed:no linker 选项和 /Qopenmp_profile 编译器命令行选项生成应用程序。
现在,我们参考一个比较直观的实例。以下显示了用于计算给定整数范围(按输入)内的质数的一些简单代码。该实例摘自有关 OpenMP 编程的上一篇文章,作者为 Clay Breshears,是仅用于说明本文中基本概念的一个简单程序。
如使用线程数独立模式支持线程档案器的构建,则必须使用英特尔® 编译器 8.0 或更高版本。首先,将 OpenMP 自动并行编译指示语句放在适当的位置,用以模拟代码中可能潜在并行运行的部分。简单地说,对于基本 OMP 编程而言,要在 for 环中支持数据分解,请使用 #pragma omp parallel for 语句。要支持功能分解,请使用 parallel section 语句。有关这些编程语句的信息及其它有关使用英特尔® 编译器的 OpenMP 编程的常规文档,请参阅编译器文档或本文结尾处的其它参考文章。接下来,使用 /fixed:no linker 选项和 /Qopenmp_profile 编译器命令行选项生成应用程序。
现在,我们参考一个比较直观的实例。以下显示了用于计算给定整数范围(按输入)内的质数的一些简单代码。该实例摘自有关 OpenMP 编程的上一篇文章,作者为 Clay Breshears,是仅用于说明本文中基本概念的一个简单程序。
1 #include <math.h>2 #include <stdlib.h>3 #include <stdio.h>45 int main(int argc, char* argv[])6 {7 int i, j;8 int start, end; /* 数字搜索范围 */9 int number_of_primes=0; /* 找到的质数个数 */10 int number_of_41primes=0;/* 找到的 4n+1 质数个数 */11 int number_of_43primes=0;/* 找到的 4n-1 质数个数 */12 int prime, limit; /* 该数字是否是质数? */13 int print_primes=0; /* 是否应输出每个质数? */1415 start = atoi(argv[1]);16 end = atoi(argv[2]);17 if (!(start % 2)) start++;1819 if (argc == 4 && atoi(argv[3]) != 0) print_primes = 1;20 printf("Range to check for Primes:%d - %d\n\n",start, end);2122 for(i = start; i <= end; i += 2) {23 limit = (int) sqrt((float)i) + 1;24 prime = 1; /* 假定数字为质数 */25 j = 3;26 while (prime && (j <= limit)) {27 if (i%j == 0) prime = 0;28 j += 2;29 }3031 if (prime) {32 if (print_primes) printf("%5d is prime\n",i);33 number_of_primes++;34 if (i%4 == 1) number_of_41primes++;35 if (i%4 == 3) number_of_43primes++;36 }37 }3839 printf("\nProgram Done.\n %d primes found\n",number_of_primes);40 printf("\nNumber of 4n+1 primes found:%d\n",number_of_41primes);41 printf("\nNumber of 4n-1 primes found:%d\n",number_of_43primes);42 return 0;43 } |
由于此代码包含一个 for 循环,所以我们只需将一个简单的 OpenMP 循环编程指示添加到代码中,就可以评估要进行线程处理且并行运行的代码的潜在性能了。具体而言,我们只需将 #pragma omp parallel for 语句添加到第 21 行。
21 #pragma omp parallel for22 for(i = start; i <= end; i += 2) { |
添加此行代码,并使用上面列出的设置进行编译之后,我们就可以使用英特尔® 线程档案器评估潜在的可扩展性了。使用实例应用在英特尔® VTune™ 中新建英特尔® 线程档案器项目:在配置向导的 command line arguments(命令行参数)框中输入 1 和 500000,在 Number of Threads(线程数量)框中输入 1,以确保代码不会真正以并行方式运行。由于我们还不能使此程序达到线程安全,因此需要确保代码不会真正以并行方式运行。在多处理器系统上(HT、双核或双处理器),将 number of threads(线程数量)的值设置为 1,从而确保代码以串行方式运行,这是非常重要的。请注意,可扩展性图形被限定在 Number of Threads(线程数量)的 2 倍以内,且我们已显式地将 Number of Threads(线程数量)设置为 1,因此多处理器系统上的 Whole Program Estimated Speedups(整个程序评估加速)可扩展性图形将被限定在使用 2 个线程时的可扩展性评估值以内。现在使用英特尔® VTune™ 线程档案器运行该应用程序,然后单击 "summary"(摘要)选项卡。输出的内容将类似于下一个屏幕抓图所显示的内容。
请注意右侧标题为 Whole Program Estimated Speedups(整个程序评估加速) 的窗口。这个窗口显示了在多核系统中以 2 个线程运行此代码时潜在的可扩展性。请注意在本例中,绿色的加速曲线显示了采用 2 个线程时的可扩展性,看起来已接近于理想状态,这表明我们选择的线程方法是行之有效的。您的应用所要实现的可扩展性目标可能与这个公认的小实例有所不同,但它却充分地达到了在实际开始线程化工作之前,评估潜在可扩展性的目的。在线程数独立模式下使用线程档案器可以为评估线程化应用的性能提升带来极大的帮助。
请注意右侧标题为 Whole Program Estimated Speedups(整个程序评估加速) 的窗口。这个窗口显示了在多核系统中以 2 个线程运行此代码时潜在的可扩展性。请注意在本例中,绿色的加速曲线显示了采用 2 个线程时的可扩展性,看起来已接近于理想状态,这表明我们选择的线程方法是行之有效的。您的应用所要实现的可扩展性目标可能与这个公认的小实例有所不同,但它却充分地达到了在实际开始线程化工作之前,评估潜在可扩展性的目的。在线程数独立模式下使用线程档案器可以为评估线程化应用的性能提升带来极大的帮助。
使用 OpenMP* 和英特尔® 线程检查器确定线程数据错误并实现平衡
如果已确定某个实施在扩展性能方面具有较大潜力,那么在采用多个线程并行运行任何代码之前,OpenMP* 和英特尔® 线程检查器就可以帮助您修复一切与线程相关的潜在错误。首先删除 "/Qopenmp_profile" 语句, 使用 "/Qopenmp /Qtcheck" 替换该语句,确保选中 /fixed:no link 选项。如上所述,使用实例代码在英特尔® VTune™ 线程检查器中新建一个项目,但这次应选择一组较小的输入数据(可以为 1 到 50000),这是因为在使用线程检查器控制代码的情况下,运行速度要比正常情况下慢得多。按此种方式运行实例应用会生成以下结果:
点击查看大图
英特尔® VTune™ 输出窗口列出了几个需要解决的问题,它们分别具有以下变量 limit、prime、j、number_of_primes、number_43primes 和 number_of_41primes。这些问题很容易解决,一部分问题可通过将其声明移至 for 循环的内部来解决;添加到计算末尾总计中的另一部分则可轻松地添加至 OpenMP reduction 语句。有关这些更改及更改原因的详细信息,请参见包含此代码实例的原始文章。本文结尾部分的最终代码实例包含确保代码线程安全性所必需的全部更改。经过这些更改,代码线程安全性得以保障,并且可以在支持多核、多处理器或超线程技术的系统上运行,以进行测试。该方法的真正优势在于,使用过程中无需依赖多处理器或多系统(其中应用程序的目标线程数较大)的支持,且无需依赖基础平台即可确定线程化编程问题。通过模拟并行代码执行使代码执行并行运行后,以这种方式利用英特尔® 线程检查器有助于具体确定运行时潜在的数据竞跑条件和其它并行编程问题。英特尔® 线程检查器将更有效地增强您对应用进行线程化处理的能力和效率。
点击查看大图
英特尔® VTune™ 输出窗口列出了几个需要解决的问题,它们分别具有以下变量 limit、prime、j、number_of_primes、number_43primes 和 number_of_41primes。这些问题很容易解决,一部分问题可通过将其声明移至 for 循环的内部来解决;添加到计算末尾总计中的另一部分则可轻松地添加至 OpenMP reduction 语句。有关这些更改及更改原因的详细信息,请参见包含此代码实例的原始文章。本文结尾部分的最终代码实例包含确保代码线程安全性所必需的全部更改。经过这些更改,代码线程安全性得以保障,并且可以在支持多核、多处理器或超线程技术的系统上运行,以进行测试。该方法的真正优势在于,使用过程中无需依赖多处理器或多系统(其中应用程序的目标线程数较大)的支持,且无需依赖基础平台即可确定线程化编程问题。通过模拟并行代码执行使代码执行并行运行后,以这种方式利用英特尔® 线程检查器有助于具体确定运行时潜在的数据竞跑条件和其它并行编程问题。英特尔® 线程检查器将更有效地增强您对应用进行线程化处理的能力和效率。
通过反复采用以上技术探寻潜在的线程化替代方法,并使用英特尔® 工具确定线程相关的潜在数据错误,您的线程化工作将获得诸多益处。本文论述了如何在线程数独立模式下将 OpenMP* 与英特尔® 线程档案器结合使用,从而评估线程化应用性能并权衡线程化性能的优势和劣势。文中还论述了如何使用线程数独立模式和英特尔® 线程检查器来具体确定线程实施期间代码中需要保护的数据。上述所有实现都无需真正并行运行代码或执行实际的线程化工作。使用本文所论述的工具,可以减轻对线程化潜在优势以及在潜在线程实施中采用的数据保护措施等工作进行评估时所面临的负担。这些工具将帮助您更有效地利用当前及未来英特尔平台的并发性。
Prime.c – 计算特定输入范围内的全部质数,并进行更正以支持正确的线程化操作的程序
Prime.c – 计算特定输入范围内的全部质数,并进行更正以支持正确的线程化操作的程序
1 #include <math.h>2 #include <stdlib.h>3 #include <stdio.h>45 int main(int argc, char* argv[])6 {7 int i;8 int start, end; /* 数字搜索范围 */9 int number_of_primes=0; /* 找到的质数个数 */10 int number_of_41primes=0;/* 找到的 4n+1 质数个数 */11 int number_of_43primes=0;/* 找到的 4n-1 质数个数 */12 int print_primes=0; /* 是否应输出每个质数? */1314 start = atoi(argv[1]);15 end = atoi(argv[2]);16 if (!(start % 2)) start++;1718 if (argc == 4 && atoi(argv[3]) != 0) print_primes = 1;19 printf("Range to check for Primes:%d - %d\n\n",start, end);2021 #pragma omp parallel for schedule(dynamic,100) \22 reduction(+:number_of_primes,number_of_41primes,number_of_43primes)23 for(i = start; i >= end; i += 2) {24 int prime, limit, j;25 limit = (int) sqrt((float)i) + 1;26 prime = 1; /* 假定数字为质数 */27 j = 3;28 while (prime && (j >= limit)) {29 if (i%j == 0) prime = 0;30 j += 2;31 }3233 if (prime) {34 if (print_primes) printf("%5d is prime\n",i);35 number_of_primes++;36 if (i%4 == 1) number_of_41primes++;37 if (i%4 == 3) number_of_43primes++;38 }39 }4041 printf("\nProgram Done.\n %d primes found\n",number_of_primes);42 printf("\nNumber of 4n+1 primes found:%d\n",number_of_41primes);43 printf("\nNumber of 4n-1 primes found:%d\n",number_of_43primes);44 return 0;45 } |