Fortran 中的矢量化求和
vectorized sum in Fortran
我正在使用 gfortran
和 -mavx
编译我的 Fortran
代码,并验证了一些指令是通过 objdump
向量化的,但我没有得到速度我期待的改进,所以我想确保对以下参数进行矢量化(这条指令大约占运行时间的 50%)。
我知道有些指令可以向量化,而有些则不能,所以我想确保这可以是:
sum(A(i1:i2,ir))
同样,这一行占用了大约 50% 的运行时间,因为我是在一个非常大的矩阵上执行此操作。我可以提供更多关于 为什么 我这样做的信息,但足以说明这是必要的,尽管我可以在必要时重组内存(例如,我可以将总和作为sum(A(ir,i1:i2))
如果可以将其矢量化。
这条线是矢量化的吗?我怎么知道?如果未进行矢量化,如何强制进行矢量化?
编辑:感谢评论,我现在意识到我可以通过 -ftree-vectorizer-verbose
检查此求和的向量化,并看到这是 而不是 向量化。我将代码重组如下:
tsum = 0.0d0
tn = i2 - i1 + 1
tvec(1:tn) = A(i1:i2, ir)
do ii = 1,tn
tsum = tsum + tvec(ii)
enddo
和这个 ONLY 在我打开 -funsafe-math-optimizations
时进行矢量化,但我确实看到由于矢量化又增加了 70% 的速度。问题仍然存在:为什么 sum(A(i1:i2,ir))
不矢量化,我怎样才能得到一个简单的 sum
来矢量化?
事实证明,除非包含 -ffast-math
或 -funsafe-math-optimizations
。
,否则我无法使用矢量化
我玩过的两个代码片段是:
tsum = 0.0d0
tvec(1:n) = A(i1:i2, ir)
do ii = 1,n
tsum = tsum + tvec(ii)
enddo
和
tsum = sum(A(i1:i2,ir))
这是我在 运行 使用不同的编译选项设置第一个代码片段时得到的时间:
10.62 sec ... None
10.35 sec ... -mtune=native -mavx
7.44 sec ... -mtune-native -mavx -ffast-math
7.49 sec ... -mtune-native -mavx -funsafe-math-optimizations
最后,通过这些相同的优化,我能够向量化 tsum = sum(A(i1:i2,ir))
以获得
7.96 sec ... None
8.41 sec ... -mtune=native -mavx
5.06 sec ... -mtune=native -mavx -ffast-math
4.97 sec ... -mtune=native -mavx -funsafe-math-optimizations
当我们将 sum
和 -mtune=native -mavx
与 -mtune=native -mavx -funsafe-math-optimizations
进行比较时,它显示出大约 70% 的加速。 (请注意,这些每个只有 运行 一次 - 在我们发布之前,我们将对多个 运行 进行真正的基准测试)。
虽然我确实受到了一点打击。当我使用 -f
选项时,我的值略有变化。没有它们,我的变量(v1
、v2
)的错误是:
v1 ... 5.60663e-15 9.71445e-17 1.05471e-15
v2 ... 5.11674e-14 1.79301e-14 2.58127e-15
但是通过优化,错误是:
v1 ... 7.11931e-15 5.39846e-15 3.33067e-16
v2 ... 1.97273e-13 6.98608e-14 2.17742e-14
这表明确实发生了一些不同的事情。
您的显式循环版本仍然按照与矢量化版本不同的顺序执行 FP 添加。矢量版本使用 4 个累加器,每个累加器获取每 4 个数组元素。
您可以编写源代码来匹配矢量版本的功能:
tsum0 = 0.0d0
tsum1 = 0.0d0
tsum2 = 0.0d0
tsum3 = 0.0d0
tn = i2 - i1 + 1
tvec(1:tn) = A(i1:i2, ir)
do ii = 1,tn,4 ! count by 4
tsum0 = tsum0 + tvec(ii)
tsum1 = tsum1 + tvec(ii+1)
tsum2 = tsum2 + tvec(ii+2)
tsum3 = tsum3 + tvec(ii+3)
enddo
tsum = (tsum0 + tsum1) + (tsum2 + tsum3)
这 可能 在没有 -ffast-math
的情况下矢量化。
FP add 具有多周期延迟,但每个时钟吞吐量一到两个,因此您需要 asm 使用多个向量累加器来使 FP add 单元饱和。 Skylake 每个时钟可以执行两次 FP 添加,延迟 = 4。以前的 Intel CPU 每个时钟执行一个,延迟 = 3。所以在 Skylake 上,你需要 8 个矢量累加器来使 FP 单元饱和。当然,它们必须是 256b 向量,因为 AVX 指令与 SSE 向量指令一样快,但做的工作是 SSE 向量指令的两倍。
用 8 * 8 个累加器变量编写源代码是荒谬的,所以我猜你需要 -ffast-math
,或者告诉编译器不同操作顺序的 OpenMP pragma。
明确展开您的源代码意味着您必须处理不是矢量宽度 * 展开倍数的循环计数。如果你对事物进行限制,它可以帮助编译器避免生成多个版本的循环或额外的循环 setup/cleanup 代码。
我正在使用 gfortran
和 -mavx
编译我的 Fortran
代码,并验证了一些指令是通过 objdump
向量化的,但我没有得到速度我期待的改进,所以我想确保对以下参数进行矢量化(这条指令大约占运行时间的 50%)。
我知道有些指令可以向量化,而有些则不能,所以我想确保这可以是:
sum(A(i1:i2,ir))
同样,这一行占用了大约 50% 的运行时间,因为我是在一个非常大的矩阵上执行此操作。我可以提供更多关于 为什么 我这样做的信息,但足以说明这是必要的,尽管我可以在必要时重组内存(例如,我可以将总和作为sum(A(ir,i1:i2))
如果可以将其矢量化。
这条线是矢量化的吗?我怎么知道?如果未进行矢量化,如何强制进行矢量化?
编辑:感谢评论,我现在意识到我可以通过 -ftree-vectorizer-verbose
检查此求和的向量化,并看到这是 而不是 向量化。我将代码重组如下:
tsum = 0.0d0
tn = i2 - i1 + 1
tvec(1:tn) = A(i1:i2, ir)
do ii = 1,tn
tsum = tsum + tvec(ii)
enddo
和这个 ONLY 在我打开 -funsafe-math-optimizations
时进行矢量化,但我确实看到由于矢量化又增加了 70% 的速度。问题仍然存在:为什么 sum(A(i1:i2,ir))
不矢量化,我怎样才能得到一个简单的 sum
来矢量化?
事实证明,除非包含 -ffast-math
或 -funsafe-math-optimizations
。
我玩过的两个代码片段是:
tsum = 0.0d0
tvec(1:n) = A(i1:i2, ir)
do ii = 1,n
tsum = tsum + tvec(ii)
enddo
和
tsum = sum(A(i1:i2,ir))
这是我在 运行 使用不同的编译选项设置第一个代码片段时得到的时间:
10.62 sec ... None
10.35 sec ... -mtune=native -mavx
7.44 sec ... -mtune-native -mavx -ffast-math
7.49 sec ... -mtune-native -mavx -funsafe-math-optimizations
最后,通过这些相同的优化,我能够向量化 tsum = sum(A(i1:i2,ir))
以获得
7.96 sec ... None
8.41 sec ... -mtune=native -mavx
5.06 sec ... -mtune=native -mavx -ffast-math
4.97 sec ... -mtune=native -mavx -funsafe-math-optimizations
当我们将 sum
和 -mtune=native -mavx
与 -mtune=native -mavx -funsafe-math-optimizations
进行比较时,它显示出大约 70% 的加速。 (请注意,这些每个只有 运行 一次 - 在我们发布之前,我们将对多个 运行 进行真正的基准测试)。
虽然我确实受到了一点打击。当我使用 -f
选项时,我的值略有变化。没有它们,我的变量(v1
、v2
)的错误是:
v1 ... 5.60663e-15 9.71445e-17 1.05471e-15
v2 ... 5.11674e-14 1.79301e-14 2.58127e-15
但是通过优化,错误是:
v1 ... 7.11931e-15 5.39846e-15 3.33067e-16
v2 ... 1.97273e-13 6.98608e-14 2.17742e-14
这表明确实发生了一些不同的事情。
您的显式循环版本仍然按照与矢量化版本不同的顺序执行 FP 添加。矢量版本使用 4 个累加器,每个累加器获取每 4 个数组元素。
您可以编写源代码来匹配矢量版本的功能:
tsum0 = 0.0d0
tsum1 = 0.0d0
tsum2 = 0.0d0
tsum3 = 0.0d0
tn = i2 - i1 + 1
tvec(1:tn) = A(i1:i2, ir)
do ii = 1,tn,4 ! count by 4
tsum0 = tsum0 + tvec(ii)
tsum1 = tsum1 + tvec(ii+1)
tsum2 = tsum2 + tvec(ii+2)
tsum3 = tsum3 + tvec(ii+3)
enddo
tsum = (tsum0 + tsum1) + (tsum2 + tsum3)
这 可能 在没有 -ffast-math
的情况下矢量化。
FP add 具有多周期延迟,但每个时钟吞吐量一到两个,因此您需要 asm 使用多个向量累加器来使 FP add 单元饱和。 Skylake 每个时钟可以执行两次 FP 添加,延迟 = 4。以前的 Intel CPU 每个时钟执行一个,延迟 = 3。所以在 Skylake 上,你需要 8 个矢量累加器来使 FP 单元饱和。当然,它们必须是 256b 向量,因为 AVX 指令与 SSE 向量指令一样快,但做的工作是 SSE 向量指令的两倍。
用 8 * 8 个累加器变量编写源代码是荒谬的,所以我猜你需要 -ffast-math
,或者告诉编译器不同操作顺序的 OpenMP pragma。
明确展开您的源代码意味着您必须处理不是矢量宽度 * 展开倍数的循环计数。如果你对事物进行限制,它可以帮助编译器避免生成多个版本的循环或额外的循环 setup/cleanup 代码。