硬件多线程
世界上最简单的处理器核心是什么样的?单核单线程!
现以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个时钟/线程。可以看到,时钟数直线下降。
这就达到了隐藏延迟的效果。
但需要达到上面的效果的前提是,线程切换速度足够快。做法便是给每个线程增加一点独立的存储,这样切换便类似于修改指针的指向,代价很小。