忙等待、休眠和准确性

4

我现在正在尝试编写一个简单的渲染器,它将以60Hz调用渲染方法并睡眠额外时间,为其他人节省CPU周期。

我遇到了一个简单的问题,如下所示:

while(m_Running){
    //start meassuring whole frame time
    t0 = System.nanoTime();

    render();

    //meassure time spent rendering
    t1 = System.nanoTime();

    if(t1<nextRedraw){
        try{
            diff = nextRedraw-t1;
            ms = diff/1000000;
            ns = (int) (diff-(ms*1000000));
            Thread.sleep(ms, ns);
        }catch(InterruptedException e){
            Logger.logWarning("Renderer interrupted!",e);
        }
        //while(System.nanoTime()<nextRedraw);//busy wait alternative
    }

    //meassure time spent sleeping
    t2 = System.nanoTime();
    nextRedraw = t2+m_RedrawTimeout;

    long frameTime = t2-t0;
    long renderTime = t1-t0;
    long sleepTime = t2-t1;
    long fps = 1000000000/frameTime;
}

这个程序可以运行,但是远未达到预期的60fps,而是在数值上跳来跳去。

FPS: 63 Frame: 15,7ms   Render: 2,0ms   Sleep: 13,7ms
FPS: 64 Frame: 15,5ms   Render: 2,0ms   Sleep: 13,5ms
FPS: 63 Frame: 15,7ms   Render: 2,1ms   Sleep: 13,5ms
FPS: 59 Frame: 16,7ms   Render: 2,8ms   Sleep: 14,0ms
FPS: 64 Frame: 15,5ms   Render: 2,2ms   Sleep: 13,3ms

当我使用忙等待时,结果更加一致并接近于我所需的fps目标。
FPS: 60 Frame: 16,4ms   Render: 2,0ms   Sleep: 14,5ms
FPS: 60 Frame: 16,4ms   Render: 2,0ms   Sleep: 14,4ms
FPS: 60 Frame: 16,4ms   Render: 2,0ms   Sleep: 14,4ms
FPS: 61 Frame: 16,3ms   Render: 2,4ms   Sleep: 13,8ms
FPS: 61 Frame: 16,3ms   Render: 2,1ms   Sleep: 14,2ms
FPS: 60 Frame: 16,4ms   Render: 2,0ms   Sleep: 14,4ms

我希望避免可理解的CPU缺陷,因此需要避免这种情况。我有一个想法,即睡眠时间少于必要时间,并循环处理剩余时间以达到精确的效果,但这似乎有点笨拙。
我的问题是,编译器是否以某种方式优化了忙等待,或者是否有其他方法可以达到类似的定时?
任何帮助都将不胜感激。
此致敬礼,
Vojtěch
注意:我使用了System.nanoTime();试图使事情更准确,但它没有帮助,但我不知道它有任何性能缺陷。
注意:我知道sleep相当不准确,但我没有找到其他选择。

1
你的电脑是否还在忙碌中?一个想法是,相比于繁忙的等待循环,Thread.sleep的唤醒更多地取决于线程调度器。从技术上讲,调度器可以对任何线程进行任何操作,但它可能不太可能长时间挂起活动线程,而对于线程的计划唤醒请求则更加灵活。 - yshavit
其实完全不是这样,更不用说我现在正在运行四核心i7,应该有相当宽松的调度器,在几乎空闲状态下。考虑到大型游戏都有FPS上限,而且通常会严格遵循,所以一定有某种可能性。只是Java中是否可用呢? - Vojtěch Kaiser
3
您考虑过使用计划执行服务ScheduledExecutorService来管理时间吗? - Steve C
从javadocs中:sleep(long millis)使当前正在执行的线程睡眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性永远不要在生产代码中依赖它进行定时!使用@Steve C建议的ScheduledExecutorService或更智能的自定义定时代码。 - Matt Coubrough
2个回答

1
我猜测,时间上的微小差异可能会累积成更大的差距,你可以通过使睡眠时间依赖于总时间来轻松避免这种情况。
类似这样的东西:
long basetime = System.nanoTime();
for (int i=0; m_Running; ++i) {
    ...
    nextRedraw = baseTime + i * m_RedrawTimeout;
}

如果当前的睡眠时间太短或太长,这应该会使接下来的睡眠时间变短或变长,以便你每秒钟得到的FPS只会变化不超过几毫秒(即小于1%)。

也许没有问题,只是你测量的是单帧需要多长时间。这个数字会有些变化,并不能说明FPS速率。

据我所知,在Thread.sleep中的纳秒将被完全忽略。


不知道纳秒会被忽略。这在甲骨文的Javadoc上有点误导人。你不应该去检查别人的实现是否符合Javadoc的规定。谢谢,我会测试你的方法和@Steve C提供的ScheduledExecutorService并报告结果。 - Vojtěch Kaiser
@VojtěchKaiser 我猜测,Thread.sleep应该支持纳秒,只是没有操作系统支持。关键在于“受系统计时器和调度程序的精度和准确性的限制”,这意味着“未定义”。在我的资料中,我看到纳秒并没有完全被忽略,但时间会四舍五入为整毫秒,并调用sleep(millis)ScheduledExecutorService可能正好符合您的要求,这是个好主意。如果您想要更多的灵活性,运行自己的服务将更容易(您已经接近完成了)。我相信,两者都可以很好地工作。 - maaartinus

0

所以我浏览了建议并在之前发布的代码基础上测试了这三个变体:

ScheduledExecutorService

private final ScheduledExecutorService scheduler 
    = Executors.newScheduledThreadPool(1);

//handle for killing the process
private ScheduledFuture<?> handle = null;

//meassuring actual time between redraws
private long lastRenderStart = 0;

//start will fire off new scheduler at desired 60Hz
@Override
public synchronized void start() {
    handle = scheduler.scheduleAtFixedRate(this, 
        100000000, m_RedrawTimeout, TimeUnit.NANOSECONDS);
}

public void kill(){
    if(handle!=null){
        handle.cancel(true);
    }
}

@Override
public void run(){
    //meassured render routine
    long t0 = System.nanoTime();
    render();
    long t1 = System.nanoTime();

    System.out.format("render time %.1fms\t\tframe time %.1fms\n",
        ((t1-t0)/1000000.0d),
        ((t0-lastRenderStart)/1000000.0d));
    lastRenderStart = t0;
}

这使得代码变得更简单了,而且据我所知也没有引入任何开销,但精度仍然没有达到我想要的程度。

render time 1,4ms       frame time 17,0ms
render time 1,4ms       frame time 16,0ms
render time 1,7ms       frame time 17,0ms
render time 1,3ms       frame time 17,0ms
render time 1,8ms       frame time 16,0ms
render time 14,8ms      frame time 16,9ms
render time 2,0ms       frame time 17,0ms

对齐循环

    long nextRedraw,baseTime = System.nanoTime();
    m_Running=true;
    for (long i=0; m_Running; ++i) {
        long t0 = System.nanoTime();
        render();
        long t1 = System.nanoTime();

        nextRedraw = baseTime + i * m_RedrawTimeout;

        long now = System.nanoTime();
        if(now<nextRedraw){
            try{
                Thread.sleep((nextRedraw-now)/1000000);
            }catch(InterruptedException e){
                Logger.logWarning("Renderer interrupted!",e);
            }
        }

        long t2 = System.nanoTime();

        long frameTime = t2-t0;
        long renderTime = t1-t0;
        long sleepTime = t2-t1;
        long fps = 1000000000/frameTime;
        System.out.format("FPS: %d\tFrame: %3.1fms\t"
            +"Render: %3.1fms\tSleep: %3.1fms\n", 
            fps, frameTime/1000000.0, renderTime/1000000.0, sleepTime/1000000.0);
    }

这种方法通常能够将帧时间保持在大约16.67毫秒左右,看起来比最初发布的代码更加优雅一些,但仍然可能出现1毫秒较短的波峰(可能是系统调度器中的舍入等原因)。

FPS: 60 Frame: 16,6ms   Render: 1,7ms   Sleep: 14,9ms
FPS: 60 Frame: 16,6ms   Render: 1,8ms   Sleep: 14,8ms
FPS: 63 Frame: 15,7ms   Render: 2,3ms   Sleep: 13,4ms
FPS: 59 Frame: 16,7ms   Render: 1,8ms   Sleep: 14,8ms
FPS: 63 Frame: 15,6ms   Render: 2,0ms   Sleep: 13,7ms
FPS: 60 Frame: 16,7ms   Render: 1,9ms   Sleep: 14,7ms
FPS: 60 Frame: 16,6ms   Render: 1,9ms   Sleep: 14,7ms
FPS: 59 Frame: 16,8ms   Render: 1,8ms   Sleep: 15,0ms
FPS: 64 Frame: 15,6ms   Render: 1,9ms   Sleep: 13,7ms
FPS: 60 Frame: 16,6ms   Render: 1,9ms   Sleep: 14,7ms
FPS: 60 Frame: 16,6ms   Render: 1,8ms   Sleep: 14,8ms

带忙等待段的对齐循环

在这个例子中,我尝试将忙等待段添加到循环中,在线程精确休眠一段时间后等待剩余时间。

    long nextRedraw,baseTime = System.nanoTime();
    m_Running=true;
    for (long i=0; m_Running; ++i) {
        long t0 = System.nanoTime();
        render();
        long t1 = System.nanoTime();

        nextRedraw = baseTime + i * m_RedrawTimeout;

        if(t1<nextRedraw){
            //if sleepy time is bigger than 1ms, use Thread.sleep
            if(nextRedraw-t1>1000000){
                try{
                    Thread.sleep(((nextRedraw-t1-1000000)/1000000));
                }catch(InterruptedException e){
                    Logger.logWarning("Renderer interrupted!",e);
                }
            }
            t2 = System.nanoTime();
            //do busy wait on last ms or so
            while(System.nanoTime()<nextRedraw);
        }

        long t3 = System.nanoTime();

        long frameTime = t3-t0;
        long renderTime = t1-t0;
        long sleepTime = t2-t1;
        long busyWaitTime = t3-t2;
        long fps = 1000000000/frameTime;
        System.out.format("FPS: %d\tFrame: %3.1fms\t"
                + "Render: %3.1fms\tSleep: %3.1fms\tBusyW: %3.1fms\n", 
                fps, frameTime/1000000.0, renderTime/1000000.0, 
                sleepTime/1000000.0,busyWaitTime/1000000.0);
    }

看起来有点凌乱,对此我很抱歉,但是通过忙等待大约2毫秒的CPU时间,我得到了相当稳定的60FPS计时。

FPS: 60 Frame: 16,5ms   Render: 1,5ms   Sleep: 13,8ms   BusyW: 1,2ms
FPS: 60 Frame: 16,5ms   Render: 1,4ms   Sleep: 13,2ms   BusyW: 1,8ms
FPS: 60 Frame: 16,4ms   Render: 1,6ms   Sleep: 12,4ms   BusyW: 2,5ms
FPS: 60 Frame: 16,5ms   Render: 1,2ms   Sleep: 13,1ms   BusyW: 2,1ms
FPS: 60 Frame: 16,5ms   Render: 1,4ms   Sleep: 13,3ms   BusyW: 1,8ms
FPS: 60 Frame: 16,4ms   Render: 1,4ms   Sleep: 12,5ms   BusyW: 2,5ms
FPS: 60 Frame: 16,5ms   Render: 1,2ms   Sleep: 13,1ms   BusyW: 2,2ms
FPS: 60 Frame: 16,5ms   Render: 1,3ms   Sleep: 13,4ms   BusyW: 1,8ms
FPS: 60 Frame: 16,4ms   Render: 1,5ms   Sleep: 12,5ms   BusyW: 2,5ms

非常感谢大家的建议,希望这能在某些时候帮到某些人 :)

祝好,

Vojtěch


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接