我编写了一个小程序,可以创建随机噪声并将其全屏显示(5K 分辨率)。我用 pygame 来实现。但刷新速度却非常慢。 surfarray.blit_array 和随机生成都需要花费大量时间。有什么办法可以加快这个速度吗?我也可以灵活地使用 julia 或 golang 来代替。或者还有psychopy或octave与psychotoolbox(但是这些似乎不能在linux/wayland下工作)。
这是我写的:
import pygame
import numpy as N
import pygame.surfarray as surfarray
from numpy import int32, uint8, uint
def main():
pygame.init()
#flags = pygame.OPENGL | pygame.FULLSCREEN # OpenGL does not want to work with surfarray
flags = pygame.FULLSCREEN
screen = pygame.display.set_mode((0,0), flags=flags, vsync=1)
w, h = screen.get_width(), screen.get_height()
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial" , 18 , bold = True)
# define a variable to control the main loop
running = True
def fps_counter():
fps = str(int(clock.get_fps()))
fps_t = font.render(fps , 1, pygame.Color("RED"))
screen.blit(fps_t,(0,0))
# main loop
while running:
# event handling, gets all event from the event queue
for event in pygame.event.get():
# only do something if the event is of type QUIT
if event.type == pygame.QUIT:
# change the value to False, to exit the main loop
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
return
array_img = N.random.randint(0, high=100, size=(w,h,3), dtype=uint)
surfarray.blit_array(screen, array_img)
fps_counter()
pygame.display.flip()
clock.tick()
#print(clock.get_fps())
# run the main function only if this module is executed as the main script
# (if you import this as a module then nothing is executed)
if __name__=="__main__":
# call the main function
main()
我需要至少 30 fps 的刷新率才能发挥作用
生成随机数的成本很高。当随机数生成器 (RNG) 需要统计上准确时(即随机数即使经过一些变换也需要看起来非常随机),并且数字是按顺序生成时,尤其如此。
事实上,对于密码学用途或某些数学(蒙特卡罗)模拟,目标 RNG 需要足够先进,以便几个后续生成的随机数之间不存在统计相关性。实际上,执行此操作的软件方法非常昂贵,以至于现代主流处理器提供了一种通过“特定指令”来执行此操作的方法。但并非所有处理器都支持这一点,并且 AFAIK Numpy 不使用它(当然是为了可移植性,因为在多台机器上具有相同种子的随机序列预计会给出相同的结果)。 幸运的是,RNG 在大多数其他用例中通常不需要那么准确。他们只需要看起来很随意。有
很多方法可以做到这一点(例如 Mersenne Twister、Xoshiro、Xorshift、PCG/LCG)。 RNG 的性能、准确性和专业化之间经常需要进行权衡。由于 Numpy 需要提供相对准确的通用 RNG(尽管据我所知并不意味着用于加密用例),因此其性能次优也就不足为奇了。 这里提供了对许多不同方法的有趣回顾(尽管结果应该持保留态度,特别是在性能方面,因为 SIMD 友好等细节对于许多用例中的性能至关重要)。
在纯 Python(使用 CPython)中实现非常快速的随机数生成器是不可能的,但可以使用import numba as nb
@nb.njit('uint64(uint64,)')
def xorshift64_step(seed):
seed ^= seed << np.uint64(13)
seed ^= seed >> np.uint64(7)
seed ^= seed << np.uint64(17)
return seed
@nb.njit('uint64()')
def init_xorshift64():
seed = np.uint64(np.random.randint(0x10000000, 0x7FFFFFFF)) # Bootstrap
return xorshift64_step(seed)
@nb.njit('(uint64, int_)')
def random_pixel(seed, high):
# Must be a constant for sake of performance and in the range [0;256]
max_range = np.uint64(high)
# Generate 3 group of 16 bits from the RNG
bits1 = seed & np.uint64(0xFFFF)
bits2 = (seed >> np.uint64(16)) & np.uint64(0xFFFF)
bits3 = seed >> np.uint64(48)
# Scale the numbers using a trick to avoid a modulo
# (since modulo are inefficient and statistically incorrect here)
r = np.uint8(np.uint64(bits1 * max_range) >> np.uint64(16))
g = np.uint8(np.uint64(bits2 * max_range) >> np.uint64(16))
b = np.uint8(np.uint64(bits3 * max_range) >> np.uint64(16))
new_seed = xorshift64_step(seed)
return (r, g, b, new_seed)
@nb.njit('(int_, int_, int_)', parallel=True)
def pseudo_random_image(w, h, high):
res = np.empty((w, h, 3), dtype=np.uint8)
for i in nb.prange(w):
# RNG seed initialization
seed = init_xorshift64()
for j in range(h):
r, g, b, seed = random_pixel(seed, high)
res[i, j, 0] = r
res[i, j, 1] = g
res[i, j, 2] = b
return res
代码相当大,但在我的 6 核 i5-9600KF CPU 上,它比 Numpy 快约 22 倍。请注意,类似的代码可以在 Julia 中使用,以便获得快速实现(因为 Julia 使用类似于 Numba 的基于 LLVM 的 JIT)。
在我的机器上,这足以达到75 FPS(最大),而初始代码达到 16 FPS。
更快的操作和渲染 在大多数平台上,生成新的随机数组
受到页面错误速度帧在 GPU 上渲染,因此 CPU 需要发送/复制 GPU 上的缓冲区,通常通过独立 GPU 的 PCIe 互连。对于宽屏幕来说,这个操作不是很快。 实际上,您可以使用着色器
(或 OpenCL/CUDA 等工具)直接在 GPU 上生成随机图像。这可以避免上述开销,并且 GPU 可以比 CPU 更快地完成此任务。