🪢

Linuxでスレッドのコンテキスト切り替えのたびにFPUのレジスタはメモリに保存されるのか

2023/02/14に公開

Linuxでスレッドのコンテキスト切り替えのたびにFPUのレジスタはメモリに保存されるのか?
シングルコアプロセッサしかなかった時代とマルチコアの時代で挙動が変わっています。

chatGPTに聞いてみた

https://twitter.com/tetsu_koba/status/1624953424063365121?conversation=none
https://twitter.com/tetsu_koba/status/1624953848291905537?conversation=none
https://twitter.com/tetsu_koba/status/1624954481598418944?conversation=none

間違えたことを言ったと気がついたら、すぐに謝ってくれるのはいい奴ですね、

スクリーンショットでは見にくいので、実際の内容を以下に貼り付けました。

In Linux kernel context switch does it save all FPU regsters each time?

In Linux, the kernel does not save all floating-point unit (FPU) registers every time a context switch occurs. This is because saving and restoring the FPU state can be expensive in terms of performance.

Instead, Linux uses a technique called "lazy FPU saving," where the FPU state is saved only when the process that is currently running actually uses the FPU. This means that if the process never uses the FPU, its FPU state will never be saved or restored during a context switch. This can greatly reduce the overhead associated with context switches.

When a process that has not used the FPU is about to be switched out, the kernel checks the "lazy FPU saving" flag. If the flag is set, it means that the process has not used the FPU, and the FPU state does not need to be saved. If the flag is not set, it means that the process has used the FPU, and the FPU state must be saved before the process is switched out.

This optimization helps to minimize the overhead associated with context switches, allowing the system to run more efficiently and handle more processes concurrently.

Is that true in multi-core ?

Yes, the same "lazy FPU saving" technique is used in multi-core systems as well. The idea behind lazy FPU saving is to avoid the overhead of saving and restoring the FPU state as much as possible. In a multi-core system, each core operates independently, and the same technique is used to manage the FPU state for each individual core.

In a multi-core system, each core has its own set of FPU registers, and each process running on a particular core can potentially use the FPU of that core. The kernel must keep track of the FPU state for each process running on each core, and must save and restore the appropriate FPU state when a context switch occurs.

By using the "lazy FPU saving" technique, the kernel can minimize the number of times it needs to save and restore the FPU state, even in a multi-core system. This helps to reduce the overhead associated with context switches and improve overall system performance.

I thoght "lazy FPU saving" is not used in multi-core systems. When does it change?

My apologies, you are correct. "Lazy FPU saving" is not used in modern multi-core systems.

In multi-core systems, each core has its own set of FPU registers, and it is possible for multiple processes to be executing on different cores simultaneously. In such cases, it is not possible to use the "lazy FPU saving" optimization, as the FPU state must be saved and restored every time a context switch occurs, regardless of whether the process has used the FPU or not.

This change was made to ensure correct behavior in a multi-core environment, where multiple processes can potentially use the FPU simultaneously. By saving and restoring the FPU state every time a context switch occurs, the kernel can guarantee that each process will have a correct and up-to-date view of the FPU state, even in a multi-core system.

I apologize for the confusion in my previous answer. Thank you for bringing this to my attention.

私の英語は文法的には微妙だしタイプミスもしていたのですが、ちょんと意図は汲み取ってくれて回答しています。英語のトレーニングにちょうどいいですね。

Lazy FPU saving とは

コンテキストスイッチのたびに毎回FPUレジスタの退避、復帰をするのはオーバーヘッドが大きいということで、必要になるまでそれを遅らせようというものです。
具体的な方法としては、  コンテキストスイッチのときにはFPUの制御レジスタの設定でFPUをアクセス禁止にします。そして実際にFPUにアクセスしたときに例外が発生します。その例外の中で、FPUレジスタのアクセスを許可しFPUレジスタの退避、復帰を行います。全てのスレッドがFPUを使うわけではないので、とても合理的に思えます。
しかし、それはシングルコアプロセッサが前提でした。
マルチコアの場合は、以前実行していたコアとは別のコアでスレッドの実行が再開されることがあり得ます。そのときにFPUレジスタの内容を復帰させようとしても、それがメモリに書き出されていなくて別のコアのFPUにあるままだと困ってしまいます。そのために、マルチコアの場合にはコンテキストスイッチのたびにFPUレジスタの退避を行うように変更されました。復帰のほうは今まで通りFPUのアクセスによって例外が発生したときです。

私はこれを10年くらい前にマルチコアのARMの評価ボードにLinuxカーネルを移植するときにソースコードを調べて得た知識です。

ソースコードを調べてみた

10年前とは状況も変わっていると思うので、実際にlinux 6.1.11のソースコートを追いかけてどうなっているか調べてみました。

arm(32bit)では、当時のコードと特に変わっていませんでした。マルチコアでない場合には"Lazy FPU saving"を行います。マルチコアの場合は上記の通り、コンテキストスイッチのときにFPUの退避を毎回行います。

arm64では、マルチコアは否かにかかわらず FPUレジスタを毎回保存するようになっていました。つまり、"Lazy FPU saving"を行わないようになっていました。
arch64をシングルコアで使うという状況はレアであるということで無視されているようです。また、ビルドオプションにFPUが無い場合という選択肢がありません。arm64は必ずFPUを備えているということになっています。
ただし、割と最近追加されたSVE (Scalable Vector Extension)の場合には"Lazy FPU saving"を行うようになっていました。[1]

riscvも調べてみました。DIRTY(=使っていた)の場合のみFPUのレジスタを保存するという判断が入っていました。"Lazy FPU saving" を行いません。

x86_64は(個人的にあまり興味がないので歴史的な事情がいろいろ絡みあってコードが難解であることが多いので)ソースコードを追っていませんが、結論だけ言うと毎回保存しています。つまり、"Lazy FPU saving" を行いません。

以下はそのメモです。

arch/arm

__switch_to アセンブラ
arch/arm

include/asm/vfpmacros.h
VFPレジスタの保存
VFPFSTMIA
VFPレジスタの復帰
VFPFLDMIA

arch/arm/vfp/vfphw.S
vfp_save_state

arch/arm/vfp/vfpmodule.c
vfp_notifier()

arch/arm/vfp/vfpmodule.c
	case THREAD_NOTIFY_SWITCH:
		fpexc = fmrx(FPEXC);

#ifdef CONFIG_SMP
		cpu = thread->cpu;

		/*
		 * On SMP, if VFP is enabled, save the old state in
		 * case the thread migrates to a different CPU. The
		 * restoring is done lazily.
		 */
		if ((fpexc & FPEXC_EN) && vfp_current_hw_state[cpu])
			vfp_save_state(vfp_current_hw_state[cpu], fpexc);
#endif

		/*
		 * Always disable VFP so we can lazily save/restore the
		 * old state.
		 */
		fmxr(FPEXC, fpexc & ~FPEXC_EN);
		break;

vfp_pm_suspend --> power management
vfp_sync_hwstate
kernel_neon_begin

arch/arm/kernel/entry-armv.S
__switch_to

arch/arm/include/asm/switch_to.h
switch_to(prev, next, last)

kernel/sched/core.c
context_switch()

arch/arm64

arch/arm64/include/asm/fpsimdmacros.h
fpsimd_save (アセンブラのマクロ)

.macro fpsimd_save state, tmpnr
	stp	q0, q1, [\state, #16 * 0]
	stp	q2, q3, [\state, #16 * 2]
	stp	q4, q5, [\state, #16 * 4]
	stp	q6, q7, [\state, #16 * 6]
	stp	q8, q9, [\state, #16 * 8]
	stp	q10, q11, [\state, #16 * 10]
	stp	q12, q13, [\state, #16 * 12]
	stp	q14, q15, [\state, #16 * 14]
	stp	q16, q17, [\state, #16 * 16]
	stp	q18, q19, [\state, #16 * 18]
	stp	q20, q21, [\state, #16 * 20]
	stp	q22, q23, [\state, #16 * 22]
	stp	q24, q25, [\state, #16 * 24]
	stp	q26, q27, [\state, #16 * 26]
	stp	q28, q29, [\state, #16 * 28]
	stp	q30, q31, [\state, #16 * 30]!
	mrs	x\tmpnr, fpsr
	str	w\tmpnr, [\state, #16 * 2]
	mrs	x\tmpnr, fpcr
	str	w\tmpnr, [\state, #16 * 2 + 4]
.endm

arch/arm64/kernel/entry-fpsimd.S
fpsimd_save_state();

arch/arm64/kernel/fpsimd.c
fpsimd_save()

arch/arm64/kernel/fpsimd.c
fpsimd_thread_switch()

arch/arm64/kernel/process.c
__switch_to

include/asm-generic/switch_to.h
switch_to(prev, next, last)

kernel/sched/core.c
context_switch()

SVEレジスタ

arch/arm64/include/asm/fpsimdmacros.h
sve_save

arch/arm64/kernel/entry-fpsimd.S
sve_save_state()

arch/arm64/kernel/fpsimd.c
fpsimd_save(void)

arch/arm64/kernel/fpsimd.c
	if (IS_ENABLED(CONFIG_ARM64_SVE) && save_sve_regs) {
		/* Get the configured VL from RDVL, will account for SM */
		if (WARN_ON(sve_get_vl() != vl)) {
			/*
			 * Can't save the user regs, so current would
			 * re-enter user with corrupt state.
			 * There's no way to recover, so kill it:
			 */
			force_signal_inject(SIGKILL, SI_KERNEL, 0, 0);
			return;
		}

		sve_save_state((char *)last->sve_state +
					sve_ffr_offset(vl),
			       &last->st->fpsr, save_ffr);
	} else {
		fpsimd_save_state(last->st);
	}

arch/riscv

arch/riscv/kernel/fpu.S
__fstate_save

ENTRY(__fstate_save)
	li  a2,  TASK_THREAD_F0
	add a0, a0, a2
	li t1, SR_FS
	csrs CSR_STATUS, t1
	frcsr t0
	fsd f0,  TASK_THREAD_F0_F0(a0)
	fsd f1,  TASK_THREAD_F1_F0(a0)
	fsd f2,  TASK_THREAD_F2_F0(a0)
	fsd f3,  TASK_THREAD_F3_F0(a0)
	fsd f4,  TASK_THREAD_F4_F0(a0)
	fsd f5,  TASK_THREAD_F5_F0(a0)
	fsd f6,  TASK_THREAD_F6_F0(a0)
	fsd f7,  TASK_THREAD_F7_F0(a0)
	fsd f8,  TASK_THREAD_F8_F0(a0)
	fsd f9,  TASK_THREAD_F9_F0(a0)
	fsd f10, TASK_THREAD_F10_F0(a0)
	fsd f11, TASK_THREAD_F11_F0(a0)
	fsd f12, TASK_THREAD_F12_F0(a0)
	fsd f13, TASK_THREAD_F13_F0(a0)
	fsd f14, TASK_THREAD_F14_F0(a0)
	fsd f15, TASK_THREAD_F15_F0(a0)
	fsd f16, TASK_THREAD_F16_F0(a0)
	fsd f17, TASK_THREAD_F17_F0(a0)
	fsd f18, TASK_THREAD_F18_F0(a0)
	fsd f19, TASK_THREAD_F19_F0(a0)
	fsd f20, TASK_THREAD_F20_F0(a0)
	fsd f21, TASK_THREAD_F21_F0(a0)
	fsd f22, TASK_THREAD_F22_F0(a0)
	fsd f23, TASK_THREAD_F23_F0(a0)
	fsd f24, TASK_THREAD_F24_F0(a0)
	fsd f25, TASK_THREAD_F25_F0(a0)
	fsd f26, TASK_THREAD_F26_F0(a0)
	fsd f27, TASK_THREAD_F27_F0(a0)
	fsd f28, TASK_THREAD_F28_F0(a0)
	fsd f29, TASK_THREAD_F29_F0(a0)
	fsd f30, TASK_THREAD_F30_F0(a0)
	fsd f31, TASK_THREAD_F31_F0(a0)
	sw t0, TASK_THREAD_FCSR_F0(a0)
	csrc CSR_STATUS, t1
	ret
ENDPROC(__fstate_save)

fstate_save()

static inline void fstate_save(struct task_struct *task,
			       struct pt_regs *regs)
{
	if ((regs->status & SR_FS) == SR_FS_DIRTY) {
		__fstate_save(task);
		__fstate_clean(regs);
	}
}

__switch_to_aux()

static inline void __switch_to_aux(struct task_struct *prev,
				   struct task_struct *next)
{
	struct pt_regs *regs;

	regs = task_pt_regs(prev);
	if (unlikely(regs->status & SR_SD))
		fstate_save(prev, regs);
	fstate_restore(next, task_pt_regs(next));
}

include/asm/switch_to.h
switch_to()

kernel/sched/core.c
context_switch()

脚注
  1. SVEはスーパーコンピューター富嶽に使われています。https://car.watch.impress.co.jp/docs/news/1315489.html ↩︎

Discussion