优化
编译器必须很小心地对程序只使用安全的优化
乍一看,这两个过程似乎有相同的行为。
不过,考虑xp等于yp的情况
内存别名使用:两个指针可能指向同一个内存位置
函数调用。大多数编译器不会试图判断一个函数是否没有副作用
每元素的周期数CPE
千兆赫兹GHz。“4GHz”处理器,这表示处理器时钟运行频率为每秒4X10 9个周期
void psum1(float a[], float p[],long n)
{
long i;
p[0] = a[0];
for(i = 1; i < n; i++)
p[i] = p[i-1]+a[i];
}
void psum2(float a[], float p[],long n)
{
long i;
p[0] = a[0];
for(i = 1; i < n-1; i+=2){
float mid_val = p[i-1] + a[i];
p[i] = mid_val;
p[i+1] = mid_val + a[i+1];
}
if(i<n)
p[i] = p[i-1] + a[i];
}
循环展开
psuml和psum2的运行时间(用时钟周期为单位)分别近 似于等式368 + 9.0n和368 + 6.0n。
psum2的CPE为6.0,优于CPE为9. 0的psum1
void combine1(vec_ptr v,data_t *dest)
{
long i;
*dest = IDENT;
for(int i = 0; i < vec_length(v); i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
void combine2(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
*dest = IDENT;
for(i = 0; i < length; i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
代码移动
执行多次(例如在循环里)但是计算结果不会改变的计算
data_t *get_vec_start(vec_ptr v)
{
return v->data;
}
void combine3(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
*dest = IDENT;
for(i = 0; i < length; i++){
*dest = *dest OP data[i];
}
}
减少过程调用,性能并没有明显提升
combine3
void combine4(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for(i = 0; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
消除不必要的内存引用
由于内存别名使用,编译器不会将combine3转换为combine4,如下
combine3(v, get_vec_start(v) + 2);
combine4(v, get_vec_start(v) + 2);
理解现代处理器
延迟界限:在下一条指令开始之前,这条指令必须结束。代码数据相关限制处理器利用指令级并行的能力时
吞吐量界限:刻画处理器功能单元原始计算能力。程序性能的终极限制
超标量(superscalar), 意思是它可以在每个时钟周期执行多个操作,而且是乱序的(out-oforder),意思就是指令执行的顺序不一定要与它们在机器级程序中的顺序一致
指令控制单元(Instruction Control Unit, ICU)和执行单元(Execution Unit, EU)
Intel Core i7 Haswell参考机有8个功能单元
0:整数运算、浮点乘、整数和浮点数除法、分支
1:整数运算、浮点加、整数乘、浮点乘
2:加载、地址计算
3:加载、地址计算
4:存储
5:整数运算
6:整数运算、分支
7:存储、地址计算
退役单元记录正在进行的处理。
一旦一条指令的操作完成了,而且所有引起这条指令的 分支点也都被确认为预测正确,那么这条指令就可以退役了,有对程序寄存器 的更新都可以被实际执行了
如果引起该指令的某个分支点预测错误,这条指令会被清空(,丢弃所有计算出来的结果。通过这种方法,预测错误就不会改变程序的状态了。
任何对程序寄存器的更新只会在指令退役时才会发生
寄存器重命名:操作数在执行单元传送。
一条更新寄存器r的指令译码时,产生标记t,条目(r,t)被加入表中。随后以r为操作数时,发送t。
重命名表只包含未进行写操作的寄存器条目
延迟:完成需要时间
发射时间:两个连续的同类型运算之间需要最小时钟周期数
容量:执行该运算功能单元数量
吞吐量:发射时间的导数,容量c,发射时间I,吞吐量C/I
发射时间为1的称为完全流水线化的
CPE
由于只有两个加载单元,导致一周期只能读两个数据,吞吐量界限0.50
访问的寄存器分为4类:只读、只写、局部(单次迭代)、循环(多次迭代)
void combine5(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for(i = 0; i < limit; i+=2){
acc = (acc OP data[i]) OP data[i+1];
}
for(; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
循环展开:一个循环按任意因子k进行展开,由此每次产生k×1循环展开,上限设为n-k+1
提高并行性
我们将累积值放在一个单独的变量acc中,会有数据相关
void combine6(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length-1;
data_t *data = get_vec_start(v);
data_t acc0 = IDENT;
data_t acc1 = IDENT;
for(i = 0; i < limit; i+=2){
acc0 = acc0 OP data[i];
acc1 = acc1 OP data[i+1];
}
for(; i < length; i++){
acc0 = acc0 OP data[i];
}
*dest = acc0 OP acc1;
}
两次循环展开 2×2循环展开
重新结合变换
void combine7(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = get_vec_start(v);
data_t acc =IDENT;
for(i = 0; i < limit; i+=2){
acc = acc OP (data[i] OP data[i+1]);
}
for(; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
combine5 12 acc = (acc OP data[i]) OP data[i+1];
combine7 12 acc = acc OP (data[i] OP data[i+1]);
2×1a的循环展开形式
只有一个数据相关
限制因素
寄存器溢出
x86-64有16个寄存器,16个ymm寄存器保存浮点数
分支预测
不要过分关心可预测的分支
这些检测总是确定索引是在界内,所以是高度可预测的
void combine4(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for(i = 0; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
void combine4b(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t acc = IDENT;
for(i = 0; i < length; i++){
if(i >=0 && i < v->len){
acc = acc OP v->data[i];
}
}
*dest = acc;
}
对于整数加法来说,会慢一点
条件传送
void minmax1(long a[], long b[],long n){
long i;
for(i = 0; i < n; i++){
if(a[i]>b[i]){
long t = a[i];
a[i] = b[i];
b[i] = t;
}
}
}
void minmax2(long a[],long b[],long n){
long i;
for(i = 0; i < n; i++){
long min = a[i]<b[i]?a[i]:b[i];
long max = a[i]<b[i]?b[i]:a[i];
a[i] = min;
b[i] = max;
}
}
minmax1 CPE 13.50 minmax2 CPE 4.0
内存性能
typedef struct ELE{
struct ELE *next;
long data;
} list_ele, *list_ptr;
long list_len(list_ptr ls){
long len = 0;
while(ls){
len++;
ls = ls->next;
}
return len;
}
.L3:
addq $1, %rax
movq (%rdi), %rdi
testq %rdi, %rdi
jne .L3
movq是循环中关键瓶颈
CPE 4.0 与L1cache的4周期访问时间一致
存储的性能
void clear_array(long *dest, long n){
long i;
for(i = 0; i < n; i++)
dest[i] = 0;
}
CPE1.0
写读相关
void write_read(long *src, long *dst, long n)
{
long cnt = n;
long val = 0;
while(cnt){
*dst = val;
val = (*src)+1;
cnt--;
}
}
CPE 7.3
程序剖析
Unix提供一个剖析程序GPROF,确定程序每个函数花费多少cpu时间,计算函数调用次数
1)程序必须为剖析而编译和链接,确保不优化
gcc -Og -pg prog.c -o prog
2)像往常一样执行
./prog file.txt
大约慢两倍,产生gmon.out
3)调用gprof来分析
gprof prog
第一部分是执行各个函数花费的时间
第二部分是函数调用历史
本节代码
#include <bits/stdc++.h>
#define IDENT 1
#define OP +
using namespace std;
void psum1(float a[], float p[],long n)
{
long i;
p[0] = a[0];
for(i = 1; i < n; i++)
p[i] = p[i-1]+a[i];
}
void psum2(float a[], float p[],long n)
{
long i;
p[0] = a[0];
for(i = 1; i < n-1; i+=2){
float mid_val = p[i-1] + a[i];
p[i] = mid_val;
p[i+1] = mid_val + a[i+1];
}
if(i<n)
p[i] = p[i-1] + a[i];
}
typedef long data_t;
typedef struct{
long len;
data_t *data;
}vec_rec, *vec_ptr;
vec_ptr new_vec(long len)
{
vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec));
data_t *data = NULL;
if(!result)
return NULL;
result->len = len;
if(len > 0){
data = (data_t *)calloc(len, sizeof(data_t));
if(!data){
free((void *)result);
return NULL;
}
}
result->data = data;
return result;
}
int get_vec_element(vec_ptr v,long index, data_t *dest)
{
if(index < 0 || index >= v->len)
return 0;
*dest = v->data[index];
return 1;
}
long vec_length(vec_ptr v)
{
return v->len;
}
void combine1(vec_ptr v,data_t *dest)
{
long i;
*dest = IDENT;
for(int i = 0; i < vec_length(v); i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
void combine2(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
*dest = IDENT;
for(i = 0; i < length; i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
data_t *get_vec_start(vec_ptr v)
{
return v->data;
}
void combine3(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
*dest = IDENT;
for(i = 0; i < length; i++){
*dest = *dest OP data[i];
}
}
void combine4(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for(i = 0; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
void combine4b(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t acc = IDENT;
for(i = 0; i < length; i++){
if(i >=0 && i < v->len){
acc = acc OP v->data[i];
}
}
*dest = acc;
}
void combine5(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for(i = 0; i < limit; i+=2){
acc = (acc OP data[i]) OP data[i+1];
}
for(; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
void combine6(vec_ptr v,data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length-1;
data_t *data = get_vec_start(v);
data_t acc0 = IDENT;
data_t acc1 = IDENT;
for(i = 0; i < limit; i+=2){
acc0 = acc0 OP data[i];
acc1 = acc1 OP data[i+1];
}
for(; i < length; i++){
acc0 = acc0 OP data[i];
}
*dest = acc0 OP acc1;
}
void combine7(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = get_vec_start(v);
data_t acc =IDENT;
for(i = 0; i < limit; i+=2){
acc = acc OP (data[i] OP data[i+1]);
}
for(; i < length; i++){
acc = acc OP data[i];
}
*dest = acc;
}
void minmax1(long a[], long b[],long n){
long i;
for(i = 0; i < n; i++){
if(a[i]>b[i]){
long t = a[i];
a[i] = b[i];
b[i] = t;
}
}
}
void minmax2(long a[],long b[],long n){
long i;
for(i = 0; i < n; i++){
long min = a[i]<b[i]?a[i]:b[i];
long max = a[i]<b[i]?b[i]:a[i];
a[i] = min;
b[i] = max;
}
}
typedef struct ELE{
struct ELE *next;
long data;
} list_ele, *list_ptr;
long list_len(list_ptr ls){
long len = 0;
while(ls){
len++;
ls = ls->next;
}
return len;
}
void clear_array(long *dest, long n){
long i;
for(i = 0; i < n; i++)
dest[i] = 0;
}
void write_read(long *src, long *dst, long n)
{
long cnt = n;
long val = 0;
while(cnt){
*dst = val;
val = (*src)+1;
cnt--;
}
}
int main()
{
}