您可以在Youtube上观看演示,在Pouet 上下载可执行文件,或从Github获取源代码。
4K简介是一个演示,其中整个程序(包括任何数据)为4096字节或更小,因此重要的是,代码应尽可能高效。Rust在构建肿的可执行文件方面享有一定声誉,因此我想看看它是否是高效而简洁的代码。
组态
整个介绍是结合Rust和glsl编写的。 Glsl用于渲染,但是Rust可以做其他事情:世界创建,相机和对象控制,工具创建,音乐播放等。
代码中的某些依赖项还没有包含在稳定的Rust中,因此我使用工具箱每晚铁锈。要安装和使用此默认捆绑包,请运行以下rustup命令:
rustup toolchain install nightly
rustup default nightly
我正在使用Crinkler压缩Rust编译器生成的目标文件。
我还使用了着色器缩小器
glsl
对着色器进行了预处理,以使其更小,更易于折叠。着色器压缩器不支持将输出输出到.rs,因此我将原始输出并手动将其复制到我的shader.rs文件中(事后看来,很明显我需要以某种方式使这一步骤自动化。甚至为着色器压缩器编写拉请求) ...
起点是我过去在Rust上进行的4K入门,那在当时似乎很简单。该文章还提供了有关配置文件
toml
以及如何使用xargo编译微型二进制文件的更多详细信息。
优化程序设计以减少代码
许多最有效的尺寸优化都不是明智的选择。这是对设计进行重新思考的结果。
在我最初的项目中,代码的一部分创建了世界,包括球体的放置,另一部分负责移动球体。在某个时候,我意识到布局代码和球体移动代码做的事情非常相似,您可以将它们组合为一个更复杂的函数,同时完成这两个任务。不幸的是,这样的优化使代码不那么优雅和可读性差。
汇编代码分析
在某些时候,您必须查看编译的汇编器,并弄清楚将代码编译成什么,以及值得进行哪些大小优化。 Rust编译器有一个非常有用的选项,
--emit=asm
用于输出汇编代码。以下命令创建一个汇编文件.s
:
xargo rustc --release --target i686-pc-windows-msvc -- --emit=asm
您不需要成为汇编专家就可以从汇编程序的输出中受益,但是对语法有基本的了解绝对会更好。此选项
opt-level = "z
强制编译器针对最小大小尽可能优化代码。在那之后,要弄清楚汇编代码的哪一部分与Rust代码的哪一部分相对应要困难一些。
我发现Rust编译器在缩小,删除未使用的代码和不必要的参数方面非常出色。它也做一些奇怪的事情,因此不时研究组装结果非常重要。
附加功能
我已经使用了两个版本的代码。一个记录过程,并允许观看者操纵相机创建有趣的轨迹。Rust允许您为这些附加动作定义功能。该文件
toml
具有[features]节,可让您声明可用的功能及其依赖关系。在toml
我的介绍4K中,具有以下配置文件:
[features]
logger = []
fullscreen = []
这些附加函数都没有依赖项,因此它们有效地充当了条件编译标志。条件代码块前面有一条语句
#[cfg(feature)]
。单独使用函数不会使代码变小,但是当您在不同的函数集之间轻松切换时,会使开发过程变得更加容易。
#[cfg(feature = "fullscreen")]
{
// ,
}
#[cfg(not(feature = "fullscreen"))]
{
// ,
}
检查编译后的代码后,我确定仅包括所选功能。
该功能的主要用途之一是为调试版本启用日志记录和错误检查。加载代码和编译glsl着色器通常会失败,并且如果没有有用的错误消息,将很难发现问题。
使用get_unchecked
将代码放在块中时,
unsafe{}
我有点假设所有安全检查都将被禁用,但事实并非如此。所有常规检查仍在此进行,而且价格昂贵。
默认情况下,range检查对数组的所有调用。采取以下Rust代码:
delay_counter = sequence[ play_pos ];
在查找表之前,编译器将插入代码,以检查play_pos是否未在序列末尾建立索引,如果发生错误,则进行恐慌。因为可能有许多这样的功能,所以这增加了代码的大小。
让我们将代码转换如下:
delay_counter = *sequence.get_unchecked( play_pos );
这告诉编译器不要进行任何范围检查,而只是查找表。这显然是危险的操作,因此只能在代码中执行
unsafe
。
更有效的循环
最初,我所有的循环都使用Rust在语法中按预期运行
for x in 0..10
。我认为它将在尽可能紧密的循环中进行编译。令人惊讶的是,事实并非如此。最简单的情况:
for x in 0..10 {
// do code
}
将被编译为执行以下操作的汇编代码:
setup loop variable
loop:
, end
//
loop
end:
而下面的代码
let x = 0;
loop{
// do code
x += 1;
if x == 10 {
break;
}
}
直接编译为:
setup loop variable
loop:
//
, loop
end:
注意,在每个循环的末尾检查条件,因此无条件跳转是不必要的。这只节省了一个周期的空间,但是当程序中有30个周期时,它们确实可以节省很多。
Rust惯用循环的另一个更难解决的问题是,在某些情况下,编译器添加了一些额外的迭代器设置代码,这些代码实际上使代码变得blo肿。我还没有想出什么原因造成这种额外的迭代器的设置,因为它一直是琐碎替换结构
for {}
构造loop{}
。
使用向量指令
我花了很多时间来优化代码
glsl
,最好的优化之一(通常也可以使代码更快地工作)是同时处理整个向量,而不是依次处理每个组件。
例如,射线追踪代码使用快速网格遍历算法来检查每条射线正在访问地图的哪些部分。原始算法分别考虑每个轴,但是您可以重写它,以便它同时考虑所有轴,并且不需要任何分支。Rust实际上并没有像glsl这样的向量类型,但是您可以使用内部函数告诉它使用SIMD指令。
要使用内置函数,我将转换以下代码
global_spheres[ CAMERA_ROT_IDX ][ 0 ] += camera_rot_speed[ 0 ]*camera_speed;
global_spheres[ CAMERA_ROT_IDX ][ 1 ] += camera_rot_speed[ 1 ]*camera_speed;
global_spheres[ CAMERA_ROT_IDX ][ 2 ] += camera_rot_speed[ 2 ]*camera_speed;
到这个:
let mut dst:x86::__m128 = core::arch::x86::_mm_load_ps(global_spheres[ CAMERA_ROT_IDX ].as_mut_ptr());
let mut src:x86::__m128 = core::arch::x86::_mm_load_ps(camera_rot_speed.as_mut_ptr());
dst = core::arch::x86::_mm_add_ps( dst, src);
core::arch::x86::_mm_store_ss( (&mut global_spheres[ CAMERA_ROT_IDX ]).as_mut_ptr(), dst );
会更小(可读性会更差)。不幸的是,尽管出于某种原因,它破坏了调试版本,但是在发行版本中仍然可以正常工作。显然,这里的问题出在我对Rust内部的了解,而不是语言本身。在准备下一个4K简介时,值得花更多的时间在此上,因为代码量的减少是巨大的。
使用OpenGL
有很多标准的Rust板条箱可用于加载OpenGL函数,但默认情况下,它们都可加载大量函数。每个加载的函数都占用一些空间,因为加载器需要知道其名称。Crinkler非常擅长压缩此类代码,但无法完全消除开销,因此我必须创建自己的版本
gl.rs
,其中仅包含所需的OpenGL功能。
结论
主要目标是编写一个具有竞争力的正确4K简介,并证明Rust适用于演示场景和每个字节都很重要且您确实需要低级控制的场景。通常,在此区域只考虑汇编器和C语言,另一个目标是充分利用惯用的Rust。
在我看来,我已经非常成功地完成了第一个任务。从来没有感觉到Rust会以某种方式阻碍我,或者我因为使用Rust而不是C而牺牲了性能或功能。
第二项任务不太成功。实在不应该有太多不安全的代码。
unsafe
具有破坏作用;使用它来快速执行某些操作非常容易(例如,使用可变的静态变量),但是一旦出现不安全的代码,它就会生成甚至更多的不安全代码,并且突然出现在各处。将来,unsafe
只有在别无选择时,我才会更加谨慎地使用。