铁锈网格,第2部分:Const泛型

2021-04-04 01:16:08

在第一部分中,我们使用1D和2D向量定义了网格性状并实现了它。基准显示1D向量是比嵌套的2D向量更好的选择。在这篇文章中,我们' ll写了一个使用数组而不是VEC的新实现。这应该更快!

生锈分配值的值时,它可以在堆栈或堆上分配。这个页面解释了两者之间的差异,但在这里我的概述了

需要遵循指针的数据结构转到堆。编译器可以在编译器中以编译时间计算其布局的数据结构可以继续堆栈。

这个"堆栈与堆"二进制是完美的,由"阵列与vec" DOCS状态为VEC,"它指向堆的内存......它的指针指向LEN初始化,连续元素的顺序。"好的,所以Vecs固有地使用堆和指针。这使得它们比堆叠分配的数据慢。

另一方面,由于阵列具有固定大小,因此编译器确切地知道他们需要多少RAM,并且可以将它们存储在堆栈上。缺点是阵列类型[t; n]有n个元素,不再,不少,并且你必须在编译时知道n的值。您可以使用用户输入或命令行标志确定n。

好吧,它发生了这种情况,即我的用例(光线示踪剂),我确切地知道网格的准确宽度和高度在编译时。所以我可以通过阵列支持的网格状地实现新的实现,而不是VEC。但是,有一个小问题。

在我的二进制中,我知道在编译时网格的精确宽度和高度。但是,我喜欢有网格状特质及其实施留在自己的可重复使用的图书馆。这意味着网格应该能够存储任何尺寸。毕竟,在那里&#39网格做了。然而,阵列必须在编译时具有已知的尺寸。似乎这两个要求彼此相互矛盾,直到最近,就是不可能满足它们。但幸运的是,Rust刚刚发布了一个新功能,可以确切地实现这种要求。

Rust现在支持Const Geacerics。你应该真正阅读文章来了解他们是什么,但这里我的摘要'传统的通用用途,如vec< t>,是泛型类型。通用参数T将始终由编译期间由Cricete类型或字符串替换。当您的程序编译时,在那里' s不再t,只有vec< i32>或vec< string&gt ;.例如,这个微小程序定义了一个名为最后一个函数的函数,它是yexy t的泛型。

// t可以是任何类型的fn lall

在编译时,Rustc检查最后一次被调用,并计算出右键替代T.第一次' s称为,t&'静态str。下次,t = bool。

Const泛型非常相似。但是,它们而不是泛型,它们'在恒定值上泛型,如1或33或false。例如,[BOOL; n]是一系列的n个booleans。编译代码时,n将用一些常量值替换,编译器将生成具有特定长度的数组,如[BOOL; 4]。

就像最后的混凝土类型如何在最后< t>必须在编译时已知,N的具体值必须在编译时以其着名。在Rust中,我们调用任何可以在编译时计算的值。因此,const泛型。看看'看一个例子:

// t可以是任何类型// w,h可以是任何USIZE值PUB STRACL网格< tcant w:moder w:monsize,const h:m:measize> {array:[[t; W]; H],}

在这里,我们定义了具有三个通用参数的类型网格。 T是一种普通的泛型。 W和H是通用的通过USEIZE值。这意味着在编译时,网格将具有特定的W和H值,并分配具有该宽度和高度的2D阵列。 T,W和T在网格的定义中是通用的,但必须在编译时用具体值替换,就像始终使用泛型。

既然我们知道如何制作一个泛型阵列长度的类型,让' s使用它来制作新的网格状实现。基于阵列的网格的构造函数几乎与基于VEC的网格的构造函数相同:

ichar< t,const w:USIZE,CONST H:USIZE>默认为网格< t,w,h>其中t:默认+复制,{fn default() - > self {self {array:[[t :: default(); W]; H], } }}

我们如何实际实例化这种类型?它&#39很容易!我们只需确保Rust编译器知道W和H所需的特定值。例如:

const宽度:USIZE = 300; Const Heigh:Usize = 200;设g:网格<使用,宽度,高度> =默认值:: default();

默认实现将是我们为此帖子所需的唯一构造函数。如果您' d喜欢,可以轻松地添加更多构造函数,也许是fn新的(t:t) - >返回一个网格的自我,所有细胞都具有相同的值t。网格状的实现也很相似:

ichar< t,const w:USIZE,CONST H:USIZE>网格状< t>对于网格< t,w,h> {Fn宽度(& self) - > USIZE {W} FN高度(& self) - > USIZE {H} FN GET(& self,p:point) - > & t {&自我。array [p.y] [p.x]} fn set_all_parallel< f>(& mut self,setter:f)其中f:send + sync + fn(point) - > t,t:发送,{使用人造丝:: prelude :: *;自我.Array。 par_iter_mut()。枚举()。 for_each(|(y,行)| {for(x,项目)中的行。Iter_mut()。枚举(){*项目= setter(point {x,y});}}; }}

请注意,您可以以方法(例如fn宽度)返回w,否则它是任何旧的USEIZE值。漂亮的弗雷肯'在我看来甜蜜。

现在,这个网格是2D,并且在第一部分中,我们发现2D VEC网格比1D VEC网格慢。但是它们'重新慢慢,因为2D VEC每行遵循一个指针,从而降低了CPU' S缓存命中率。 2D阵列没有那个问题,因为所有W * H元素都在概要中被布置在内存中。但要获得一些公平的基准,我们应该尝试实现基于数组的基于阵列的网格。这将需要一些Const Geachics的东西,即实际稳定。这意味着,在我的生活中第一次,我将不得不使用......夜间生锈。

要获取一个可以存储网格中的所有元素的1D数组,我们需要一个长度n的数组n = w * h.我们需要使用那些const通用值进行算术,如下所示:

理论上,这应该是完全没事的。毕竟,RUSTC将在编译时知道W和H的值,并且它可以在编译时常用两个数字。但是,在实践中,正如Const Generics团队在此解释的那样,实现此编译器功能(对于所有类型,而不仅仅是使用)非常复杂。所以该功能仍在开发中。幸运的是,我们可以安装编译器的开发版本,调用"夜间",启用此正在进行的功能。

如果你和#39; T之前,不要担心夜间编译器,担心,直到实施这种类型,既不是我!它' sase

并确保您当前的货运项目使用夜间编译器,只需从项目内部运行此:

现在,我们使用夜间编译器,我们可以启用正在进行的功能(也称为"不稳定"特征),通过将此添加到lib.rs:

好的,现在我们可以开始使用const值进行算术。 1D阵列网格的实现与1D VEC网格和2D阵列网格相当类似。一个奇怪的部分是where子句,但我' ll解释一下。

使用箱子:: {gridlike,point}; //定义数据结构Pub结构网格< tcant w:USIZE,CONST H:USIZE>其中[(); W * H]:大小,{阵列:[T; W * H],} //构造函数iclich< t,const w:UNSIZE,CONST H:USIZE>默认为网格< t,w,h>在哪里 [(); w * h]:大小,t:默认+拷贝,{fn default() - > self {self {array:[t :: default(); W * H],}}}}} //实现网格状iclic< t,const w:USIZE,CONST H:USIZE>网格状< t>对于网格< t,w,h>在哪里 [(); W * H]:大小,{//就像2D阵列Fn宽度(& self) - > USIZION {W} //就像2D阵列FN高​​度(& self) - > USIZION {H} //就像1D VEC FN GET(& self,p:point) - > & t {&自我.Array [p.y * w + p.x]} //就像1d vec fn set_all_plallel< f>(& mut self,setter:f)其中f:send + sync + fn(point) - > t,t:发送,{使用人造丝:: prelude :: *;自我.Array。 par_iter_mut()。枚举()。 for_each(|(i,项目)| {* item = setter(point {x:i%w,y:i / w});}); }}

关于这的一个奇怪的事情就是where子句,其中[(); W * H]:大小。最初我没有包括那个,但是编译器抱怨一个"不受约束的通用常量"我在Twitter上谈到了一个Rust Dev,他说,当这个Const通用算术功能结束时,赢得了Where Clareage' t。它' s有点奇怪,但嘿,这个功能仍在开发中。我'留下的是没有恐慌的,我真的没有恐慌,我真的不在于在锈队抛出这个功能时把不必要的where子句置于。

旁边:铁锈核心团队如此乐于助人。如果我推文给@rustlang帐户的问题,他们经常将其转发,以便通过可以回答的DEV看到它。防锈也是真的很有帮助。不要害怕寻求帮助!人们有友好,并将指出你的正确方向。

在运行基准之前,我花了一秒钟来进行预测。我的假设是,阵列实现比VEC实现更快,并且1D阵列将比2D阵列更快。

基准本身基本上与第一部分的基准相同,只需添加了新的阵列的实现“。我们'在三种不同的场景中重新计算4个网格状实现中的每一个(设置所有元素,按顺序获取元素,获取随机元素)。您可以在GitHub上查看完整的基准代码。请记住使用标准:: black_box以避免编译器跳过所有代码,因为它(正确但无比)Infers,它实际上对程序输出有任何差异。

至于Set基准,嗯,2D VEC是如此缓慢,使结果基本上不可读。

很有意思! 1D阵列始终比2D阵列慢。我怀疑这是因为1D阵列FN Set_All_Parallipling实现会引起一些开销,从1D和2D表示之间进行翻译。这需要一些算术与%和/(模数和分割运算符)。另一方面,似乎编译器智能足以有效地索引2D数组。并使用1D阵列并不是获得从1D VS 2D VEC看到的缓存命中率的提高性能。可能是因为一旦你'使用数组,一切都是堆叠分配的。

IT' S也完全可能错过了一些基准的微妙之处,编译器正在优化(或未能优化)一些东西。 那个'为什么人工基准只是解决方案的一部分。 现实世界的结果可能有所不同! 事实上,在我使用这些网格的光线示踪剂中,唯一的重要差异是2D VEC实现比其余的速度慢。 在我的具体用例中,释放二进制没有显示1D阵列,2D阵列和1D VEC之间的任何改进。 我最终在我的光线示踪剂中使用了2D数组,因为1D-To-2D翻译数学是一种不必要的并发症。 谢谢阅读! 我希望你喜欢学习Const Geacerics,夜间生锈和标准 - 我知道我做了。 如果您有任何疑问或建议,请在Twitter或通过电子邮件时告知我。 代码在github上,如果您有建议改进它,请随时打开PR。 谢谢!