CPU | 主频

以 Intel 的 i7 - 8700 为例,我们来看看 CPU 的主频。

如何理解主频

CPU主频越高,单核性能越强,CPU的运算速度更快。

应该如何理解?

1
程序执行时间 ≈ 程序指令数 * 指令平均时钟周期(CPI) * 单个时钟周期时间

其中在程序代码确定的情况下,即 程序指令数 * 指令平均时钟周期(CPI)得到的时钟周期总数固定,则单个时钟周期时间决定程序的执行时间,而主频决定了单个时钟周期的时间。

1
主频 = 1s / 时钟周期时间

举个栗子,假设CPU在一个时钟周期执行一条运算指令,CPU 1GHz 和 2GHz,意味着1ns和0.5ns执行1条运算指令,0.5ns相比于1ns快了一倍,自然运算速度也更快。
(但是实际上更复杂一些,指令周期包含取指令、执行指令,一个指令周期由若干个机器周期组成,机器周期由多个时钟周期组成)

CPU是以主频稳定运行的吗

要解释这个问题,先来看看CPU频率是怎么定义的。

1
2
处理器基本频率
表示处理器晶体管打开和关闭的速率。处理器基本频率是 TDP 定义的操作点。频率以千兆赫兹 (GHz) 或每秒十亿次循环计。

以 Intel 的 CPU 为例,官方给出的CPU频率为基础频率,是 TDP 定义的操作点。说简单点,即CPU在TDP功耗下,能长时间稳定运行的最大频率(注意,并不是指CPU最多只能跑到这个最大频率,后面我们会讲到)。

想要查看到实际CPU的频率,可以通过/proc/cpuinfo查看每个核心的信息,可以看到核心的频率是在不断变化的:

1
grep -E 'cpu MHz|processor' /proc/cpuinfo

为什么CPU实际频率会超过基础频率

从上图中可以发现,i7-8700 的基础频率是 3.2 GHz,而实际的核心频率已经达到 4.4 GHz 左右,超过了基础频率,原因是 Intel 的 Trubo Boost 睿频加速技术(AMD 也有类似的技术,Trubo Core),根据需要动态调节处理器频率,允许CPU在一段时间内超越它的基础频率, i7 - 8700的最大睿频频率可以达到4.6 GHz。

现代 Intel CPU 基本都支持睿频,并自动开启,同时也是可以通过配置开启或关闭的,

1
echo 1 > /sys/devices/system/cpu/intel_pstate/no_trubo

默认为0,表示开启睿频,配置为1则关闭睿频,关闭后,CPU频率稳定在 3.2 GHz左右

(超频也可以让 CPU 实际频率超过基础频率,需要 CPU 和主板支持)

除了CPU的频率可以调整吗

答案当然是肯定的,为了实现CPU调频,Linux 内核提供了 cpufreq 子模块来完成这一目的。

cpufreq 子模块

该模块包含四个部分:Core Framework 核心框架、Scaling Governor 调频器、Scaling Driver 调频驱动、Scaling Policy 调频策略,它们之间的关系如下:

(1) 核心框架,提供通用的代码框架和接口来支持CPU调频

linux/include/cpufreq.h 中定义了数据结构和接口来

(2) 调频器,实现了不同的算法来评估所需的CPU频率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct cpufreq_governor {

	char       name[CPUFREQ_NAME_LEN];
	int        (*init)(struct cpufreq_policy *policy);
	void       (*exit)(struct cpufreq_policy *policy);
	int        (*start)(struct cpufreq_policy *policy);
	void       (*stop)(struct cpufreq_policy *policy);
	void       (*limits)(struct cpufreq_policy *policy);
	ssize_t    (*show_setspeed)	(struct cpufreq_policy *policy, char *buf);
	int        (*store_setspeed)	(struct cpufreq_policy *policy, unsigned int freq);

	bool               dynamic_switching;
	struct list_head   governor_list;
	struct module      *owner;

};

代码展示了调频器设计的核心数据结构和方法,方法均为函数指针,不同的调频器可以自由定义实现

(3) 调频驱动,与硬件直接通信,获取调频器所需要的效能状态,提供接口进行调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cpufreq_driver {

  char      name[CPUFREQ_NAME_LEN];
  u8		flags;
  void		*driver_data;

  int		(*init)(struct cpufreq_policy *policy);
  int		(*verify)(struct cpufreq_policy_data *policy);
  int		(*setpolicy)(struct cpufreq_policy *policy);
  int		(*online)(struct cpufreq_policy *policy);
  int		(*offline)(struct cpufreq_policy *policy);
  int		(*exit)(struct cpufreq_policy *policy);
  void      (*stop_cpu)(struct cpufreq_policy *policy);
  int		(*suspend)(struct cpufreq_policy *policy);
  int		(*resume)(struct cpufreq_policy *policy);
  void      (*ready)(struct cpufreq_policy *policy);

  ...
};

在配置中,cpufreq 提供了一些通用的调频器,不同的调频器提供了功能不同的调频算法,用于不同的场合。

performance 会在 scaling_max_freq 限制的范围内,尽可能进入高频率状态
powersave 会在 scaling_min_freq 限制的范围内,尽可能进入低频率状态
userspace 该调频器不做任何配置,允许通过 scaling_setspeed 自定义 CPU 频率
ondemand 定时基于 CPU 负载进行频率动态设置的方式,负载低时降频,负载高时升频(系统在忙和闲之间切换频繁时效果并不好)
conservative 与 ondemand 类似,定时基于 CPU 负载进行调频,不同在于频率调整采用逐步递变的方式。
schedutil 基于 CPU 使用率,利用内核机制 - utilization update callback, 通过负载变化回调机制来进行调频的方法(相比于 ondemand 和 conservative 的定时获取,能更快获取 CPU 负载变化进行调整)

(4) 每个 CPU 核心独有一份调频策略,保存有当前 CPU 频率状态、调频器、调频驱动和策略配置等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct cpufreq_policy {

	cpumask_var_t		cpus;	/* Online CPUs only */
	cpumask_var_t		related_cpus; /* Online + Offline CPUs */
	cpumask_var_t		real_cpus; /* Related and present */
	unsigned int		cpu;    /* cpu managing this policy, must be online */
	struct             cpufreq_cpuinfo	cpuinfo;/* current cpu info */
	unsigned int		min;    /* min freq in kHz */
	unsigned int		max;    /* max freq in kHz */
	unsigned int		cur;    /* cur freq in kHz, only needed if cpufreq governors are used */
	unsigned int		policy; /* see above */
	unsigned int		last_policy; /* policy before unplug */
	struct             cpufreq_governor	*governor; /* see below */
	void			    *governor_data; /* scaling governor */
	char			    last_governor[CPUFREQ_NAME_LEN]; /* last governor used */
	struct             cpufreq_stats	*stats;
	void			    *driver_data; /* scaling driver */
	.....
};

在了解 cpufreq 的结构后,我们来看看如何调整 CPU 频率。

(1) 使用 cpufreq_policy 来调整 CPU 频率

在内核初始化时,cpufreq 会创建 sysfs 目录来展示 cpufreq_policy 调频策略中的部分信息,

ls /sys/devices/system/cpu/cpufreq/policy{X}

其中,{X} 对应 CPU 核心的编号,每一个 CPU 核心都是独立的调频策略。 (对应核心的 policy 也链接到了/sys/devices/system/cpu/cpu{X}/cpufreq)

调整策略也包含一些通用的属性,cpuinfo_* 记录的是CPU硬件支持的频率信息,scaling_* 表示通过 cpufreq 进行扩展调节的所支持频率、配置等信息。
(数据定义对应 cqufreq.h 中的 cpufreq_policy )

cpuinfo_min_freq、cpuinfo_max_freq CPU支持的最小、最大频率
cpuinfo_cur_freq 从硬件读取到的CPU当前实际频率。如果这个值不确定的话,可能不会展示。(我测试的时候是没有的)
cpuinfo_transition_latency 采用 policy 进行效能状态转换所花费的时间(ns)
affected_cpus 属于当前 policy 的 online cpu
related_cpus 属于当前 policy 的 所有 cpu,包含 online 和 offline
scaling_available_governors 当前内核提供的可用调频器或驱动提供调频算法
scaling_cur_freq 最后一次通过调频驱动获得的 CPU 频率,而非当前时刻的频率。
scaling_driver 当前使用的调频驱动
scaling_governor 当前 policy 使用的调频器或调频算法,该值可修改
scaling_min_freq、scaling_max_freq 当前 policy 允许的最小、最大频率,该值可修改(kHz)
scaling_setspeed 当使用 userspace 调频器时可用,可配置 cpu 频率(kHz)

通过 scaling_setspeed,我们可以对CPU频率进行设置,但是其受限于调频器,只有 userspace 才可以进行 CPU 频率自定义。

这是在我本地查看0号核心的输出结果,当前核心频率944 MHz,在频率范围上与i7 - 8700 的官方定义一致,调频器使用的 powersave,调频驱动使用的是intel_psate,在使用 powersave 调频器的情况下,scaling_setspeed 处于 unsupported 状态,而支持的调速器也不包含 userspace。这种情况下,需要通过其他方式来进行频率调整。

(2) 使用 cpufreq_driver 来调整 CPU 频率

在 Linux 内核源码中,intel_pstate.c 记录了其内部实现。既然是调频驱动,则必须实现 cpufreq 定义的接口,intel_pstate 定义的接口实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct cpufreq_driver intel_pstate = {
	.flags		= CPUFREQ_CONST_LOOPS,
	.verify		= intel_pstate_verify_policy,
	.setpolicy	= intel_pstate_set_policy,
	.suspend	= intel_pstate_suspend,
	.resume		= intel_pstate_resume,
	.init		= intel_pstate_cpu_init,
	.exit		= intel_pstate_cpu_exit,
	.stop_cpu	= intel_pstate_stop_cpu,
	.offline	= intel_pstate_cpu_offline,
	.online		= intel_pstate_cpu_online,
	.update_limits	= intel_pstate_update_limits,
	.name		= "intel_pstate",
};

在 intel_pstate_set_policy 中,会设置生效调频策略,并通过 intel_pstate_update_perf_limits 更新 CPU 能耗配置

1
2
3
4
5
6
7
int max_freq = intel_pstate_get_max_freq(cpu);

static int intel_pstate_get_max_freq(struct cpudata *cpu)
{
	return global.turbo_disabled || global.no_turbo ?
			cpu->pstate.max_freq : cpu->pstate.turbo_freq;
}

其中,max_freq 取决于是否开启睿频,未开启则取默认的最大频率,即基础频率;已开启则使用睿频频率。通过睿频可以提升 CPU 频率上限,也符合我们之前实验的结果。可是由此看来,好像没有其他方式来修改 CPU 的频率。

细看代码,通过 intel_pstate_update_perf_limits 的实现,可以发现CPU 的实际性能表现并不仅仅是通过 max_freq 决定的,还涉及一个参数 max_policy_perf,用于设定 CPU 的最大性能比例。

1
2
3
global_max = DIV_ROUND_UP(turbo_max * global.max_perf_pct, 100);
cpu->max_perf_ratio = min(max_policy_perf, global_max);
cpu->max_perf_ratio = max(min_policy_perf, cpu->max_perf_ratio);

通过 intel_pstate 提供的配置入口可以进行修改

1
2
/sys/devices/system/cpu/intel_pstate/max_perf_pct
(值范围[0, 100])

该值实际是设置最大的性能百分比,max_perf_pct = 50 可以限制其主频最多跑到 max_freq 的 50%。

CPU 频率对我们的程序有什么影响

来做个简单的测试,代码如下:

1
2
3
4
5
6
7
8
9
10
func main() {
	loop := uint64(150000000000)
	sum := uint64(0)
	startTime := time.Now().UnixNano()
	for index := uint64(0); index < loop; index++ {
		sum += index
	}
	endTime := time.Now().UnixNano()
	fmt.Println("Time cost in mills : ", (endTime - startTime) / 1000000)
}

代码逻辑很简单,做循环加法,纯 CPU 运算,最后计算耗时(nano)。在不 桶的CPU 频率配置下,我们来看看结果:

核心频率 核心使用率 计算耗时(ms)
4.57 GHz 100% 33581
3.2 GHz 100% 47130
2.0 GHz 100% 71848

随着主频下降,计算耗时逐渐上升,且主频下降的比例与计算耗时的增长比例相近。主频决定了 CPU 的运算速度,因此当主频降低时,计算耗时也相应增加。

CPU 频率和使用率之间的关系

这一点上往往容易产生误区,会觉得主频越高,使用率一定高,或者使用率越高,主频也一定高。实际上,两者的关系有点像水管和水流的关系,CPU频率类似水管大小,使用率是水流,水管可以很大,水流可以很小。

还是上面的例子,只是在每次循环加之后,增加一个sleep:

1
2
3
4
5
6
7
8
9
10
11
func main() {
	loop := uint64(150000000000)
	sum := uint64(0)
	startTime := time.Now().UnixNano()
	for index := uint64(0); index < loop; index++ {
		sum += index
		time.Sleep(100)
	}
	endTime := time.Now().UnixNano()
	fmt.Println("Time cost in mills : ", (endTime - startTime) / 1000000)
}

因为每次循环计算都会sleep,所以 CPU 实际跑不满的:

本地测试CPU使用率在60%左右,而主频已经达到了4.3 GHz左右的睿频频率,此时相当于水管很大,但是水流只占水管大小的60%左右。

如果我们对 CPU 进行降频,把频率将至3.2GHz左右,相同搞定代码下,结果则会不一样:

此时同样的运算在降频后,CPU 使用率达到了100%,相当于使用了更小的水管后,水流跑满了整个水管。