CPU - 主频

1,990 阅读9分钟

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

如何理解主频

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

应该如何理解?

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

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

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

举个栗子,假设CPU在一个时钟周期执行一条运算指令,CPU 1GHz 和 2GHz,意味着 1ns 和 0.5ns 执行1条运算指令,0.5ns 相比于 1ns 快了一倍,自然运算速度也更快。

(但是实际上更复杂一些,指令周期包含取指令、执行指令,一个指令周期由若干个机器周期组成,机器周期由多个时钟周期组成)

CPU是以主频稳定运行的吗

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

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

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

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

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 基本都支持睿频,并自动开启,同时也是可以通过配置开启或关闭的:

// Linux
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频率

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) 调频驱动。与硬件直接通信,获取调频器所需要的效能状态,提供接口进行调整

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 频率状态、调频器、调频驱动和策略配置等

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_freqCPU支持的最小、最大频率
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号核心的输出结果,和官方文档中的描述是一致的。

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

之前我们提到,只有调频驱动使用userspace时,才能自定义调整 CPU 频率,而当前的调频驱动是powersave,有没有其他方式可以进行调整呢?

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

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 能耗配置

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 频率上限,也符合我们之前实验的结果。

同时,我们可以通过 intel_pstate_update_perf_limits 的实现,发现 CPU 的实际性能表现并不仅仅是通过 max_freq 决定的,还涉及一个参数 max_policy_perf,用于设定 CPU 的最大性能比例,以达到修改 CPU 频率的目的。

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 提供的配置入口可以进行修改

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

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

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

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

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 GHz100%33581
3.2 GHz100%47130
2.0 GHz100%71848

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

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

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

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

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%,相当于使用了更小的水管后,水流跑满了整个水管。

总结

本文简单介绍了 CPU 主频及 Linux 内核中的主频控制部分。
之所以会想了解这个部分,主要是会进行中间件压测,需要跑满 CPU ,所以会比较好奇 CPU 各个参数对其实际性能表现的影响,同时在自己进行 CPU 选购时,也更清楚什么样的场景,在不同主频和核心数的 CPU 选择上应该如何判断。