基线实现应该是可预测的

2021-05-15 06:38:34

我写了互惠的,因为在RustData依赖行为中没有找到Div-By-By-By-By-By-By的良好实现。为什么我关心?

喜欢荒谬的鱼在他对M1和Xeon的整数分裂的评论中,某些除数(当舍入一小部分时丢失了大量精确性的人(N / 2 ^ K \))需要不同,较慢,代码经典实现的路径。两种的力量也是通常的,但至少转移到更快的顺序,变量右移。

互惠反而使用统一的代码路径来实现两个表达式,\(f_ {m,s}(x)=左\ lfloor \ frac {mx} {2 ^ s} \ rectle \ rfloor \)和\(g_ {m ^ \ prime,s ^ \ prime}(x)= \ left \ lfloor \ frac {m ^ \ prime \ cdot \ min(x + 1,\ mathtt {u64 :: max})} {2 ^ {s ^ \ prime } \ \ r = rfloor \),除了\(x \)中的饱和递增\(g_ {m ^ \ prime,s ^ \ prime}(x)\)之外是相同的。

第一个表达式,\(f_ {m,s}(x)\)对应于互换\(llvm,libdivide等)的usualdiv-by-mul近似(在gcc,llvm,libdivide等中实现)。近似互换\(1 / d \)在通过舍入\(m \)向上的固定点中,通过运行时在TruncationMultiplation补偿的向上误差。例如,通过不变的整数使用乘法,参见例子和正常的划分。

第二个,\(g_ {m ^ \ prime,s ^ \ prime}(x)是通过n位乘法添加的n位无符号划分中的乘法和addscheme。

在该近似值中,当将\(1 / d ^ \ prime \)转换为固定点时,互酷乘数\(m ^ \ prime \)。 Atruntime,我们然后碰到产品(通过最大值\(\ FRAC {N} {2 ^ {s ^ \ Prime}}< 1 / d ^ \ prime \),即\(\ frac {m ^ \ Prime} {2 ^ {s ^ \ prime}} \))在丢弃低位之前。

有了一点代数,我们看到了那个\(m ^ \ prime x + m ^ \ prime = m ^ \ prime(x + 1)\)......并且我们可以使用饱和的增量来避免64x65乘法,只要我们不在't触发该分散器的第二个表达式\(d ^ \ prime \)for \(\ left \ lfloor \ frac {\ mathtt {u64 :: max}} {d ^ \ prime} \ levity \ rfloor \ neq \ left \ lfloor \ frac {\ mathtt {u64 :: max} - 1} {d ^ \ prime} \ rectle \ rfloor \)。

我们有一对双近似,将倒数倒入固定点值的一个双近似值,另一个是舍入的;它始终绕过最近的额定精度,与始终申请一个或另一个案例相比,这是一个额外的精度。

幸运的是,1个U64 :: Max的因素(1和U64 :: max除外)与“圆形”近似值工作不递增,因此饱和增量始终是安全的,我们实际上希望使用第二个“倒倒” “近似(除非\(d ^ \ prime \ in \ {1,\ mathtt {u64 :: max} \)))。

甚至更好,\(f_ {m,s} \)和\(g_ {m ^ \ prime,s ^ \ prime} \)仅在缺席或存在饱和增量的情况下不同而不是分支,互酷性执行数据-drive incrownby 0或1,对于\(f_ {m,s}(x)\)分别分别为\(g_ {m ^ \ prime,s ^ \ prime}(x)).PHOT:可预测的硬件改进师,甚至由不同常数的红外。

结果摘要以下结果:在我的i7 7Y75 @ 1.3 GHz上测量依赖性部门的吞吐量,互惠一致地一致地每分师1.3 ns,而硬件师只能达到〜9.6 ns /句(互惠需求量减少14% )。这个看起来与鱼类报告的结果相当,因为在7.鱼的libdivide nodoubt在更好的躺下的水分上做得更好,特别是两个,但是很好地知道简单的实施是关闭的。

我们还将看到,在Rust Land中,Fast_divide Crateis由强度_Reeduce主导,并且强度仍然只能比分为两种权力的互动(虽然,看着拆卸,但它可能会接近单一结果延迟)。

首先,分割的结果具有相同的预先计算。 Thetimings来自Criterion.rs,在紧密循环中为\(10 ^ 4 \)分区。

最后两个选项是我在写作之前考虑的箱子。强度_ReDuce在特殊情况下切换两个(实现为Bitscan和Shift)的特写箱,以及处理具有128位固定点乘数的一切的ageneral慢路径。 Fast_Divide由Libdivide Andimplements的启发相同的三个路径:两个(Shiftright)的权力的快速路径,用于互惠乘法器的慢路径,其需要一个比特这个字大小(例如,划分乘以7),以及常规的循环-by-mul序列。

hardware_u64_div_2时间:92.297我们95.762我们100.32美] compiled_u64_div_by_2时间:2.3214美国2.3408我们2.3604我们】reciprocal_u64_div_by_2时间:12.667我们12.954我们13.261我们] strength_reduce_u64_div_by_2时间:2.8679美国2.9190我们2.9955我们】fast_divide_u64_div_by_2时间:2.7467美2.7752 US 2.8025 US]

这是互联器的比较最糟糕的情况:虽然互动路线使用相同的代码路径(1.3 ns / division),但是编译器展示可以使用右移更好地完成更好。分支实现包括两个权力的特殊情况,因此靠近Thecompiler,感谢一个可预测的分支进入右移。

hardware_u64_div_7时间:95.244我们96.096我们97.072我们] compiled_u64_div_by_7时间:10.564我们10.666我们10.778我们] reciprocal_u64_div_by_7时间:12.718我们12.846我们12.976我们] strength_reduce_u64_div_by_7时间:17.366我们17.582我们17.827我们] fast_divide_u64_div_by_7时间:25.795我们26.045 US 26.345 US]

划分乘以7很难用于通过n位乘法添加在Robison的n位无符号划分中描述的“舍入”近似的编译器。这是互联器的比较最佳情况,因为它总是相同的代码(1.3 NS / DIVATION),但大多数其他实施方式转换为慢路程(强度_REDUCE进入常规情况,即表示更复杂,但对LLVM更透明)。具有LLVM直接编译的Evencivisiver比互联网速度快20%:LLVM不实现Robison的倒核方案,因此它是一个更复杂的Sequencethan互联器。

hardware_u64_div_11时间:95.199我们95.733我们96.213我们] compiled_u64_div_by_11时间:7.0886美国7.1565我们7.2309我们】reciprocal_u64_div_by_11时间:12.841我们13.171我们13.556我们] strength_reduce_u64_div_by_11时间:17.026我们17.318我们17.692我们] fast_divide_u64_div_by_11时间:21.731我们21.918 US 22.138 US]

这是一个典型的结果。同样,互惠可以值得信任为1.3 ns / division。当DividingBy 11时,常规圆形Div-By-By-By-By-yoursfore Fine,因此由LLVM编译的代码只需要乘法和班次,几乎是互惠通用序列的两倍。 Fast_DidideCrate在这里做得比除以7时做得更好,因为它避免了TheLocest路径,但倒数仍然更快;简单付出。

高于奖励特殊外壳的三个微型发型,因为它们在循环中始终始终恒定,因此始终击中Samecode路径而不会导致错误预定的分支。

由不可预测的预测除数是由2,3,7或11(分别容易,常规,艰难和定期除数)的分区的独立部门会发生什么?

硬件_u64_div时间:[91.592 US 93.211 US 95.125 US]互联网_U64_DIV时间:[17.436 US 17.620 US 17.828 US]强度_REDUCE_U64_DIV时间:[40.477 US 41.581 US 42.891 US] FAST_DIVIDE_U64_DIV时间:[69.069 US 69.562 US 70.100 US]

硬件不关心,并且互酷性只是一个较慢的(1.8ns /划分而不是1.3 ns / division),所以可能因为现在必须在循环体中加载相关的PartialReciprocal结构。

另外两个分支实现似乎采取了特殊情况的数量的特点。强度_寿命只有分支一次,检测为两者的权力的除数;它的运行时从0.29 - 1.8 ns /划分到4.2 ns / division(至少2.4 ns慢/分部)。 fast_divide热路径,如libdivide,在三个情况下切换,并且从0.28 - 2.2ns /划分到7.0 ns / disply(至少4.8 ns慢/分部)。

这就是为什么我更愿意以可预测的基线实现:特殊情况的不可预测的代码可以很容易地在基准上易于形成,但是,在开发期间,它可以判断基准如何与真实工作量不同,以及特殊情况“超支”的特殊情况。关于这些差异。

对于分除类别的特殊情况,大多数运行时的div-by-by-mulimplementations会让您猜测您是否倾向于通过“常规”除数,或“硬”倾向于才能分开,或者通过“硬”,以便估算他们将执行。更糟糕的是,他们还强迫您陷入不同的类之间的频率.Reciprocal没有那个问题:它的热路径是恒定除数的模拟,所以它对所有除数具有相同的预测性能,3和所有除法只有一个代码路径,所以我们不必担心类。

取决于工作量,转移到更快的守护者可能是有意义的,但通常最好在没有特殊情况下开始的事情,而当我认为这样做......我认为互惠表明,它由常数普通划分,它是。

结构是“部分”,因为它不能表示1或U64 :: max的分区。 ↩ ...除了1和U64 :: max之外的所有除数,它必须使用更通用的互动结构。 ↩