硬件多线程

世界上最简单的处理器核心是什么样的?单核单线程!

现以GPU为例。

渲染1024个顶点,也就是1024个线程。渲染的Shader程序由两条指令组成,且都是算术逻辑指令。所有算术逻辑指令的执行都花费一个时钟周期。

FragThread threads[1024];
for (auto thread : threads) {
    for (auto inst : thread.insts()) {
        inst.execute(1, ALU); // inst execute on an ALU for 1 cycle
    }
}

那么,渲染完1024个顶点需要花费2048个时钟周期,也就是1024个线程 x 2条指令/线程 x 1个时钟/指令。

如果把Shader程序改复杂一点,将其中一条指令改为加载纹理数据指令。

一般来说,访问显存所花费的时钟数几百到上千不等。不妨假设执行一条加载纹理数据指令花费999个时钟。

FragThread threads[1024];
for (auto thread : threads) {
    for (auto inst : thread.insts()) {
        if (inst is arithmetic operation)
            inst.execute(1, ALU); // inst execute on an ALU for 1 cycle
        if (inst is texture operation)
            inst.execute(999, TU); // inst execute on an TU for 999 cycles
    }
}

此时,渲染完1024个顶点需要花费102400个时钟周期,也就是1024个线程 x 1000个时钟/线程。可以看到,时钟数是之前的500倍!

不难想到,如果像加载纹理数据这一类型的指令比较多的情况下,我们的处理器执行效率将非常低。

优化!

线程0执行到加载指令后,不傻傻等待立马切换线程,让线程1继续从头开始执行直到也执行到加载指令,接着再切换到线程2执行,直到最后一个线程1023。接下来,又回到线程0的执行,这时候线程0请求的纹理数据已经加载回来了,因为已经过去了1024个时钟,而加载只需要999个时钟,线程0立马结束。后面的线程以此类推。

此时,渲染完1024个顶点需要花费3072个时钟周期,也就是1024个线程 x 3个时钟/线程。可以看到,时钟数直线下降。

这就达到了隐藏延迟的效果。

但需要达到上面的效果的前提是,线程切换速度足够快。做法便是给每个线程增加一点独立的存储,这样切换便类似于修改指针的指向,代价很小。

posted @ 2020-12-12 12:27  专注于GPU的程序员  阅读(678)  评论(0编辑  收藏  举报