使用Array Notation的利弊

概述

C++ 数组符号是英特尔® Cilk™ Plus英特尔® C++ Composer XE 的一种特性的一部分。数组符合是表达并行性的一种方式。数组符号可在向量化方面为编译器提供帮助。但是,用户在使用它时必须要谨慎。数组表达式经常要求创建中间数组(在评估表达式时使用)的临时拷贝。其中一个负面影响是,这些临时向量会从高速缓存中溢出,因此无法重复利用并且会导致性能低于同等的原始循环。在较短的向量中重新编写数组句法可避免高速缓存溢出问题。

主题

由于向量长度而引起的数组符号折中

将代码转换为数组符号时了解目标硬件可直接支持哪些操作是非常有用的。考虑下面的标量代码:

view sourceprint?

01

void scalar(T *R, T *A, T *B, int S, T k) {

02

  // S is size __assume_aligned(R,64);

 

03

  __assume_aligned(A,64);

04

  __assume_aligned(B,64);

 

05

  for (int i = 0; i < S; i++) {

06

    T tmp = A[i] * k - B[i];

 

07

    if (tmp > 5.0f) {

08

      tmp = tmp * sin(B[i]);

 

09

    }

10

    A[i] = tmp;

 

11

  }

12

}

如果使用基本技术即用数组符号 “[0:S]” 来替换循环索引下标 “[i]”直接将标量代码转换为数组符号该代码的结果如下

view sourceprint?

01

void longvector(T *R, T *A, T *B, int S, T k) {

02

  __assume_aligned(R,64);

 

03

  __assume_aligned(A,64);

04

  __assume_aligned(B,64);

 

05

  T tmp[S];

06

  tmp[0:S] = A[0:S] * k - B[0:S];

 

07

  if (tmp[0:S] > 5.0f) {

08

    tmp[0:S] = tmp[0:S] * sin(B[0:S]);

 

09

  }

10

  A[0:S] = tmp[0:S];

 

11

}

如果数组大小 "S" 较大大于二级高速缓存),那么上面的代码可能无法实现最佳的执行效果因为数组段太大。特别是:

1) 临时数组 "tmp"原始代码中的一个标量值现在是一个较大的数组。该数据在算法中多次使用,但是并不适用于高速缓存,必须从内存重新加载。它甚至可能会导致堆栈分配出现问题。

2) 数组 "B" 存在相同的问题它可以重复使用但是并不适用于高速缓存。

3) 大小 "S" 数组段操作比硬件向量长度大得多。编译器必须决定如何对其划分以实现高效的向量化操作。

编译器可以将所有代码融合在一起以提高重复利用率但这可能会受到两个因素的影响即未知大小 “S”以及以通用的方式将数组声明为 "kpointers to T"

对激进编译器分析依赖程度更低的代码编写方式为

view sourceprint?

01

void shortvector(T *R, T *A, T *B, int S, T k) {

02

  __assume_aligned(R,64);

 

03

  __assume_aligned(A,64);

04

  __assume_aligned(B,64);

 

05

  for (int i = 0; i < S; i += VLEN) {

06

    T tmp[VLEN];

 

07

    tmp[:] = A[i:VLEN] * k - B[i:VLEN];

08

    if (tmp[:] > 5.0f) {

 

09

      tmp[:] = tmp[:] * sin(B[i:VLEN]);

10

    }

 

11

    A[i:VLEN] = tmp[:];

12

  }

 

13

}

这种短向量样式重新引入 for-loop并以 VLEN 元素组的形式通过循环进行迭代。在该循环中,编译器将生成相关的操作以便在某个时刻处理 VLEN 元素。如果选择 VLEN 来匹配目标硬件向量长度,这些操作可直接映射到向量指令和寄存器操作。

这里有一个明显的问题:“短向量样式不是比原始 for-loop 更难以读取吗 那么使用标量 for-loop 并依赖于编译器向量化和其它优化不是更好吗这个问题的答案是取决于具体的情况。短向量样式的优势在于,它能够准确告诉编译器会发生的情况,并帮助进行因数组符号语义而执行的依赖性分析(数组符号表示语句中没有依赖性)。如果标量 for-loop 以最佳方式执行,当然也就没有理由使用该样式。但是,如果编译器没有提供带有标量 for-loop 的代码生成,那么使用短向量来调试循环是一种很好的方法(无须考虑像 strip-mining 那样的结构)。

另外一个问题是:“VLEN 的最佳值是什么?”一条较好的经验法则是从目标硬件向量大小开始并尝试乘以或除以 2。增加大小会增加计算循环所需的向量寄存器的数量,但也可以通过减少 trip-count 以及提供更多的优化机会来提升性能。减少大小会减少 trip-count,但是如果循环操作需要更多的向量存储器,则可能需要这么做(例如,当混合浮点数和双精度数时减少 VLEN 是一种不错的方法)。对于英特尔® 至强 融核™ 协处理器,16 看上去是针对上述例程最优的 VLEN

在英特尔® 至强 融核 协处理器上上面的短向量代码的运行速度比原生标量代码提高 25%。完整代码:

view sourceprint?

01

// Example of short vector style coding vs. scalar. With benchmark timing.

02

#include <stdio.h>

 

03

#include <stdlib.h>

04

#include <math.h>

 

05

#define S 8192*4

06

#define T float

 

07

#define ITERS 100

08

#define VLEN 16

 

09

  

10

// Reduce for AVX,SSE,etc.

 

11

__declspec(noinline) void scalar(T *R,T *A, T *B, T k) {

12

  __assume_aligned(R,64);

 

13

  __assume_aligned(A,64);

14

  __assume_aligned(B,64);

 

15

  for (int i = 0; i < S; i++) {

16

    T tmp = A[i] * k - B[i];

 

17

    if (tmp > 5.0f) {

18

      tmp = tmp * sin(B[i]);

 

19

    }

20

    A[i] = tmp;

 

21

  }

22

}

 

23

// NOT EXECUTED; CAUSES STACK OVERFLOW DUE TO LARGE stack allocation

24

__declspec(noinline) void longvector(T *R,T *A, T *B, T k) {

 

25

//__declspec(noinline) void longvector(T R[S],T A[S], T B[S], T k) {

26

  __assume_aligned(R,64);

 

27

  __assume_aligned(A,64);

28

  __assume_aligned(B,64);

 

29

  T tmp[S];

30

  tmp[0:S] = A[0:S] * k - B[0:S];

 

31

  if (tmp[0:S] > 5.0f) {

32

    tmp[0:S] = tmp[0:S] * sin(B[0:S]);

 

33

  } A[0:S] = tmp[0:S];

34

}

 

35

__declspec(noinline) void shortvector(T *R,T *A, T *B, T k) {

36

  __assume_aligned(R,64);

 

37

  __assume_aligned(A,64);

38

  __assume_aligned(B,64);

 

39

  for (int i = 0; i < S; i += VLEN) {

40

    T tmp[VLEN];

 

41

    tmp[:] = A[i:VLEN] * k - B[i:VLEN];

42

    if (tmp[:] > 5.0f) {

 

43

      tmp[:] = tmp[:] * sin(B[i:VLEN]);

44

    } A[i:VLEN] = tmp[:];

 

45

  }

46

}

 

47

bool compare(T ref, T cean) {

48

  return (fabs(ref - cean) < 0.00001);

 

49

}

50

//__declspec(align(64)) T A[S],B[S],C[S];

 

51

int main() {

52

  volatile __int64 start=0, end=0, perf_ref=0, perf_short=0, max, tmpdiff;

 

53

  T *A,*B,*C;

54

  posix_memalign((void **)&A, 64, sizeof(T)*S);

 

55

  posix_memalign((void **)&B, 64, sizeof(T)*S);

56

  posix_memalign((void **)&C, 64, sizeof(T)*S);

 

57

  //__declspec(align(64)) T A[S],B[S],C[S];

58

  int short_vs_ref;

 

59

  T ref_result, short_result;

60

  float k = 0.5;

 

61

  max = 0;

62

  for (int i = 0; i < ITERS; i++) {

 

63

    A[0:S] = __sec_implicit_index(0);

64

    B[0:S] = __sec_implicit_index(0);

 

65

    C[0:S] = __sec_implicit_index(0);

66

    start = __rdtsc();

 

67

    scalar(A,B,C,k);

68

    tmpdiff = __rdtsc() - start;

 

69

    perf_ref += tmpdiff;

70

    if (max < tmpdiff) max = tmpdiff;

 

71

    ref_result = __sec_reduce_add(A[0:S]);

72

  }

 

73

  perf_ref -= max;

74

  tmpdiff = (perf_ref - perf_short) * 100 / perf_ref;

 

75

  short_vs_ref = (int)tmpdiff;

76

  if (!compare(ref_result, short_result)) {

 

77

    printf("MISCOMPARE SHORT: FAILEDn");

78

    return -1;

 

79

  } else if (short_vs_ref < 15) {

80

    printf("SHORT VECTOR IS < 15%% FASTER THAN SCALAR : %d%%n", short_vs_ref);

 

81

    printf("FAILEDn"); return -2;

82

  }

 

83

  printf("SHORT VS SCALAR SPEEDUP >= 15%%: %d%%n", short_vs_ref);

84

  printf("PASSEDn");

 

85

  return 0;

86

}

下面的文章提供了关于外层循环向量化的更多示例。

ISCA 2012 论文:《传统编程是否能缩小并行计算应用的 Ninja 性能差距2012 6

要点

C++ 数组符号是英特尔® Cilk™ Plus英特尔® C++ Composer XE 的一种特性的一部分。数组符合是表达并行性的一种方式。数组符号可在向量化方面为编译器提供帮助。但是,用户在使用它时必须要谨慎。数组表达式经常要求创建中间数组(在评估表达式时使用)的临时拷贝。其中一个负面影响是,这些临时向量会从高速缓存中溢出,因此无法重复利用并且会导致性能低于同等的原始循环。在较短的向量中重新编写数组句法可避免高速缓存溢出问题。

下一步

要在英特尔® 至强 融核架构上成功调试您的应用请务必通读此指南并点击文中的超链接查看相关内容。本指南提供了实现最佳应用性能所要执行的步骤。

返回到主章节“向量化要素? HYPERLINK "http://software.intel.com/en-us/articles/vectorization-essential" 矢量化要素

 

Einzelheiten zur Compiler-Optimierung finden Sie in unserem Optimierungshinweis.