PAC2017小经历(一)

PAC2017小经历(一)

经过了半年的学习之后,菜鸡在大腿的协助下参加了PAC2017的决赛。负责做第一道comcot海啸预测模型的优化问题(跟之前的比赛迷之相似,然而完全不同)。最后做出来效果好像还不错,在缺少了一个关键编译选项的情况下,速度提高了27.7倍(自然,大腿的功劳更大)。在这里大致写一些学到的东西,以防以后再次掉坑。

KNL平台

KNL的结构图

KNL架构的CPU菜鸡我也不能说的很清楚,丢下一个链接:http://blog.throneclay.com/2016/08/20/knl_mcdram/。其中几个比较重要的,对优化有帮助的是核心数、内存模式以及支持AVX-512的指令集,不过我们当时好像只有68个核心可以用(而且说好的超线程我们试了半天也没超起来)。

iso_c_binding

作为整个优化的第一步,我们先把热点函数改为了C(虽然后来证明这好像并不是个好主意)。因为是在icc以及ifort下进行的优化,于是就是用了iso_c_binding这个module来作为传递参数等操作的核心。

https://software.intel.com/en-us/node/678425 iso_c_binding的文档

Fortran与C混合编译的不同点

可以说的是,Fortran和C最大的不同就是多维数组的排列方式了。简单一点来说,Fortran里面是在前面的维度先发生变化,C里面是在后面的维度先发生变化。体现出来的效果就是对于一个二维数组的遍历,Fortran是像古人读书一样,一列一列的往下看,而C是像现代人一样一行一行的看。所以我们需要做的是,把Fortran里面的数组能够在C里面访问,同时保持Fortran里面的定位方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define ARRAY(type, name) type *name
#define CONST_ARRAY(type, name) const type *name
#define ARRAY_INIT_1D(type, name, s1, e1) \
(name) = (type *)_mm_malloc(sizeof(type) * ((e1) - (s1) + 1),64)
#define ARRAY_INIT_2D(type, name, s1, e1, s2, e2) \
(name) = (type *)_mm_malloc(sizeof(type) * ((e1) - (s1) + 1) * ((e2) - (s2) + 1),64)
#define ARRAY_INIT_3D(type, name, s1, e1, s2, e2, s3, e3) \
(name) = (type *)_mm_malloc(sizeof(type) * ((e1) - (s1) + 1) * \
((e2) - (s2) + 1) * ((e3) - (s3) + 1),64)
#define ARRAY_AT_1D_SIZED(name, s1, i) ((name)[((i) - (s1) + 1)])
#define ARRAY_AT_2D_SIZED(name, s2, e2, s1, i, j) \
((name)[(j - s1) * (e2 - s2 + 1) + i - s2])
#define ARRAY_AT_3D_SIZED(name, s3, e3, s2, e2, s1, i, j, k) \
((name)[(k - s1) * (e3 - s3 + 1) * (e2 - s2 + 1) + \
(j - s2) * (e3 - s3 + 1) + i - s3])
#define ARRAY_AT_2D(name, i, j) ARRAY_AT_2D_SIZED(name, 1, L_nx_act, 1, i, j)
#define ARRAY_AT_3D(name, i, j, k) \
ARRAY_AT_3D_SIZED(name, 1, L_nx_act, 1, L_ny_act, 1, i, j, k)

宏定义如上图所示,具体的道理很简单,记在这里是为了方便以后直接复制代码)。

Fortran与C变量对应关系

Fortran和C的变量大多数都是一一对应的,同时iso_c_binding提供了宏定义,以便作为对Fortran中Real或者Integer等变量的约束。同时iso_c_binding提供了更多不存在于Fortran本身中的一些宏定义,以便能够与C进行内存数据交换。

详情参见文档,在这里举几个最简单的例子:

C_INT -> int -> INTEGER(KIND = 4)

C_FLOAT -> float -> REAL(KIND = 4)

C_DOUBLE -> float -> REAL(KIND = 8)

差不多就是下面这个样子:

1
2
3
4
real(C_DOUBLE) :: L_dt
real(C_DOUBLE) :: L_rx
real(C_DOUBLE) :: L_ry
real(C_DOUBLE) :: L_grx

值得注意的是,Fortran里面的type(也就是C里面的struct)只要包含了Pointer指针就不能整体一起传参,不过可以把它全部拆开之后,把需要的传过去,然后再在最后合并即可。

在Fortran里面调用C函数

讲道理,我觉得是不会有人试图在C里面调用Fortran的函数的,于是我也就只写这个就好了。

下面先把两个demo程序贴出来,以便展示。

ffile.f90

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
program demo_c_f_pointer
use, intrinsic :: iso_c_binding
implicit none
interface
function test_pointer(s, s1) bind(C)
use, intrinsic :: ISO_C_BINDING
integer(C_INT) :: test_pointer
type(C_PTR) :: s
real, value :: s1
end function test_pointer
end interface
integer(C_INT) :: result
integer(C_INT) :: count = 0
integer :: i, j
real :: s
s = 1.8 * 2.3333333
result = test_pointer(C_LOC(s), s)
print *, L%a_pointer, count
end program demo_c_f_pointer

cfile.c

1
2
3
4
5
6
#include "stdio.h"
int test_pointer(float * s, double s1) {
printf("%f %lf", *s, s1);
return 0;
}
  1. 首先在Fortran程序主函数入口的 program 下面加上 use, intrinsic :: iso_c_binding ,使得整个程序加载iso_c_binding模块;

  2. 将想要使用的C函数通过interface写好,其中要注意一定(?)要有一个返回值,这个返回值一般选用int即可。因为如果没有返回值,Fortran将会把它作为一个subroutine来处理,并没有测试过subroutine是怎样的调用方法。不过function是可以没有参数的。

  3. function内部也要use这个模块,原因不是很懂。然后如果是参数的话,使用type(C_PTR),表明后面的变量是一个C语言中的指针。如果加上了value,表明是一个传值操作,毕竟Fortran里面所有的函数调用都是传引用。

  4. 写好了之后,就可以开开心心调用了。其中C_LOC相当于C里面的&取地址符;

  5. 编译的时候,主程序在那个语言下,就用那个语言的编译器进行链接即可。运行一下,成功!

后记

实际上,既然intel已经提供了这个iso_c_binding神器之后,也就是说它对Fortran已经是非常友好的了。之前ASC清华大学一来就全部改成C碾压我们的经历让我们也一来也改成了C,结果似乎反而有了一些反优化,同时在某些地方精度损失也相应变大了,可以说有一点得不偿失吧。不过也让我们更好的认识了这个程序最核心的部分,对后面的优化可以说也是有点帮助了。。下次还是要选择一个更好的策略才行呀。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2018 Alex's Blog All Rights Reserved.

Yifeng Tang hält Urheberrechtsansprüche.