为现代 C++ 中的模拟器编写一个以 mhz 一致且有效地触发的时钟(循环)
Writing a clock(loop) that is triggered in the mhz consistently and effeciently for an emulator in modern C++
我目前正在为旧的 CPU(intel 8085)开发模拟器。表示CPU时钟是3.2mhz。我正在尝试尽可能准确,并尽可能跨平台
对我来说,这意味着我需要一个以 3.2mhz 频率调用的时钟。我不在乎它是否太准确,只要在 10% 以内就足够了。
简单的方法是
auto _PrevCycleTime = std::chrono::high_resolution_clock::now();
double _TimeBetweenClockCycles = 1.0 / 3200000;
while (1)
{
auto now = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);
if (t.count() >= _TimeBetweenClockCycles)
{
_PrevCycleTime = now;
//Clock trigger
}
}
这会产生每秒调用 240 万次的时钟。
更大的问题是,我意识到即使 运行 一个 while(1) {}
也使用了我 CPU 的 50-60%。这样不行。
我的下一个方法是使用睡眠功能。
long long _TimeBetweenClockCyclesNS = 1000000000 / 3200000
double _TimeBetweenClockCycles = 1.0 / 3200000;
auto now = std::chrono::high_resolution_clock::now();
while (_Running)
{
std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);
if (t.count() >= _TimeBetweenClockCycles)
{
_PrevCycleTime = now;
//Clock trigger
}
else
{
std::this_thread::sleep_for(std::chrono::nanoseconds(_TimeBetweenClockCyclesNS));
}
}
虽然这(有点)有效,但它根本不一致。我希望它“足够接近”,但上面的代码被调用:
- 100.000 次/秒 Visual Studio 调试模式(<3% cpu 使用率)
- 170 万次/秒,在 Visual Studio 调试模式下使用 1 纳秒休眠时间,但再次使用约 30% cpu 使用率。
- 100 次/秒(是的,只有 100,代码中没有其他更改)在 Visual Studio 发布模式(<1% cpu 使用率)
- 即使使用
std::this_thread::sleep_for(std::chrono::nanoseconds(1))
时钟每秒也只会触发 1200 次。
我很可能遗漏了一些明显的东西,而且我把它弄得太复杂了,但我相信一定有更好的方法,因为其他“更重”系统的模拟器似乎使用更少 CPU 同时需要更高的准确性。
在我的用例中,我最关心的是:
- 跨平台,但我不介意为不同的平台编写不同的代码OS
- 没有使用大量资源
- 我不关心它是否 非常 准确,只要它不会对 CPU 的时间有太大影响即可。 (例如,如果一个“等待”程序应该等待 1 秒,我真的不介意它是 0.8 秒还是 1.2 秒)
我有哪些选择?
(注意: 在没有任何时钟限制逻辑的情况下,我的时钟每秒可以 运行 超过 3000 万次。同样,使用我的大约 50-60% CPU。所以它 应该 能够 运行 300 万次,使用率要低得多 CPU)
(注意: 代码 运行 在单独的 std::thread 中,如果重要的话)
要认识到的重要一点是,如果某些事情发生在错误的时间,没有人会注意到。你有一个 CPU 与一些外围设备交谈。假设外围设备是 GPIO 引脚。只要 CPU 在正确的时间打开和关闭 GPIO 引脚,实际上没有人会注意到 CPU 在这些时间之间是否 运行 太快。如果它驱动显示输出,只要以正确的帧速率显示,没有人会注意到显示像素是否计算得太快。等等。
仿真器使用的一项技术是计算 CPU 指令使用的时钟周期数。如果您使用的是解释型设计,则可以在指令处理程序中写入 clockCycles += 5;
。如果您使用的是 JIT 设计,则可以在每个基本块的末尾进行。如果你不是使用JIT设计,不知道什么是基本块,可以忽略上一句。
然后,当 CPU 确实做了一些重要的事情时,比如更改 GPIO 引脚,您可以睡觉以赶上进度。如果自上次休眠以来发生了 3,200,000 个时钟周期,但实际时间只有 0.1 秒,那么您可以在更新屏幕之前休眠 0.9 秒。
你拥有的“睡眠点”越少,你的时间就越不准确,你浪费在保持准确时间上的时间就越少。视频游戏模拟器通常会尽可能快地渲染整个帧。事实上,自 N64/PS1 时代以来,许多仿真器根本就懒得模拟 CPU 时序。这些系统的计时非常复杂,以至于每个游戏都已经知道它必须等待下一帧开始,所以模拟器只需要以正确的速率开始帧。
另一个想法是计算计时信息并将其发送到外设,而不实际计时。游戏确实依赖于精确显示时序的早期系统(例如 SNES)的模拟器可以全速 运行 CPU 然后告诉显示代码“在时钟周期 12345 CPU 写入 0x6789注册12。”显示代码然后可以计算显示在该时钟周期内绘制的像素,并更改其绘制方式。仍然没有必要实际同步 CPU 和显示时间。
如果您想要精确计时而又不会严重拖慢程序速度,您可能需要使用 FPGA 而不是 CPU。
我目前正在为旧的 CPU(intel 8085)开发模拟器。表示CPU时钟是3.2mhz。我正在尝试尽可能准确,并尽可能跨平台
对我来说,这意味着我需要一个以 3.2mhz 频率调用的时钟。我不在乎它是否太准确,只要在 10% 以内就足够了。
简单的方法是
auto _PrevCycleTime = std::chrono::high_resolution_clock::now();
double _TimeBetweenClockCycles = 1.0 / 3200000;
while (1)
{
auto now = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);
if (t.count() >= _TimeBetweenClockCycles)
{
_PrevCycleTime = now;
//Clock trigger
}
}
这会产生每秒调用 240 万次的时钟。
更大的问题是,我意识到即使 运行 一个 while(1) {}
也使用了我 CPU 的 50-60%。这样不行。
我的下一个方法是使用睡眠功能。
long long _TimeBetweenClockCyclesNS = 1000000000 / 3200000
double _TimeBetweenClockCycles = 1.0 / 3200000;
auto now = std::chrono::high_resolution_clock::now();
while (_Running)
{
std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);
if (t.count() >= _TimeBetweenClockCycles)
{
_PrevCycleTime = now;
//Clock trigger
}
else
{
std::this_thread::sleep_for(std::chrono::nanoseconds(_TimeBetweenClockCyclesNS));
}
}
虽然这(有点)有效,但它根本不一致。我希望它“足够接近”,但上面的代码被调用:
- 100.000 次/秒 Visual Studio 调试模式(<3% cpu 使用率)
- 170 万次/秒,在 Visual Studio 调试模式下使用 1 纳秒休眠时间,但再次使用约 30% cpu 使用率。
- 100 次/秒(是的,只有 100,代码中没有其他更改)在 Visual Studio 发布模式(<1% cpu 使用率)
- 即使使用
std::this_thread::sleep_for(std::chrono::nanoseconds(1))
时钟每秒也只会触发 1200 次。
我很可能遗漏了一些明显的东西,而且我把它弄得太复杂了,但我相信一定有更好的方法,因为其他“更重”系统的模拟器似乎使用更少 CPU 同时需要更高的准确性。
在我的用例中,我最关心的是:
- 跨平台,但我不介意为不同的平台编写不同的代码OS
- 没有使用大量资源
- 我不关心它是否 非常 准确,只要它不会对 CPU 的时间有太大影响即可。 (例如,如果一个“等待”程序应该等待 1 秒,我真的不介意它是 0.8 秒还是 1.2 秒)
我有哪些选择?
(注意: 在没有任何时钟限制逻辑的情况下,我的时钟每秒可以 运行 超过 3000 万次。同样,使用我的大约 50-60% CPU。所以它 应该 能够 运行 300 万次,使用率要低得多 CPU)
(注意: 代码 运行 在单独的 std::thread 中,如果重要的话)
要认识到的重要一点是,如果某些事情发生在错误的时间,没有人会注意到。你有一个 CPU 与一些外围设备交谈。假设外围设备是 GPIO 引脚。只要 CPU 在正确的时间打开和关闭 GPIO 引脚,实际上没有人会注意到 CPU 在这些时间之间是否 运行 太快。如果它驱动显示输出,只要以正确的帧速率显示,没有人会注意到显示像素是否计算得太快。等等。
仿真器使用的一项技术是计算 CPU 指令使用的时钟周期数。如果您使用的是解释型设计,则可以在指令处理程序中写入 clockCycles += 5;
。如果您使用的是 JIT 设计,则可以在每个基本块的末尾进行。如果你不是使用JIT设计,不知道什么是基本块,可以忽略上一句。
然后,当 CPU 确实做了一些重要的事情时,比如更改 GPIO 引脚,您可以睡觉以赶上进度。如果自上次休眠以来发生了 3,200,000 个时钟周期,但实际时间只有 0.1 秒,那么您可以在更新屏幕之前休眠 0.9 秒。
你拥有的“睡眠点”越少,你的时间就越不准确,你浪费在保持准确时间上的时间就越少。视频游戏模拟器通常会尽可能快地渲染整个帧。事实上,自 N64/PS1 时代以来,许多仿真器根本就懒得模拟 CPU 时序。这些系统的计时非常复杂,以至于每个游戏都已经知道它必须等待下一帧开始,所以模拟器只需要以正确的速率开始帧。
另一个想法是计算计时信息并将其发送到外设,而不实际计时。游戏确实依赖于精确显示时序的早期系统(例如 SNES)的模拟器可以全速 运行 CPU 然后告诉显示代码“在时钟周期 12345 CPU 写入 0x6789注册12。”显示代码然后可以计算显示在该时钟周期内绘制的像素,并更改其绘制方式。仍然没有必要实际同步 CPU 和显示时间。
如果您想要精确计时而又不会严重拖慢程序速度,您可能需要使用 FPGA 而不是 CPU。