操作顺序如何严重改变代码速度?
How Order of Operations Can Seriously Change The Code Speed?
我看了a video Tarodev 的代码优化。在视频的最后,我很震惊,因为我看到操作顺序可以严重改变代码速度。虽然自己试过了,但是不明白怎么可能呢? Float x Float x Vector 如何比 Float x Vector x Float 或 Vector x Float x Float 快 2 倍?
我从 GitHub 安装了 Tarodev 的项目并自己尝试了。结果和视频一样。
编辑:
感谢所有这些很棒的回答,伙计们。在你回答之后,我明白了原因并感到愚蠢 :) 另外,我在 Orhtej2 的 建议
中添加了 video Repository and example code
答案很简单,按照我的例子:
向量是由超过 1 个值组成的对象:例如Vector3 v = new Vector3(1,2,3)
如果您将 Vector3 的数字相乘,您将进行 3 次运算。
float n = 3.f
Vector3 v = new Vector3(1,2,3)
n*v
结果是 Vector3(1*n,2*n,3*n)
,所以一次乘法需要 3 次计算。
在第一种情况下按照您的示例进行 n*n*v
,因此 n*n + 3
计算 n^2 * v
的 1 次计算,总共 4 次计算。
在第二种情况下,你正在做 n * v * n
,这意味着 n*v
的 3 次计算 + v*n
的另外 3 次计算,总共 6 次计算。
Vector x Scalar1 x Scalar2 => (Vector x Scalar1) x Scalar2 =>
VectorTemp x Scalar2 = EndResult
总而言之,我们必须通过向量运算对标量进行两次运算。意义
向量长度乘法的 2 倍。
对比
Scalar1 x Scalar2 x Vector = (Scalar1 x Scalar2) x Vector =>
Scalar3 x Vector = EndResult.
这里需要一个Vector x Scalar运算。 Scalar2 的 Scalar1 只是 multipication
所以我想这里的关键思想是如果操作是可交换的,从最便宜的开始。
首先
someVector3 * someFloat
和
someFloat * someVector3
基本上都是return
new Vector3(someVector3.x * someFloat, someVector3.y * someFloat, someVector3.z * someFloat)
所以基本上是 3 float * float
操作 + Vector3
的构造函数调用
然后顺序很重要,因为您的操作将按这样的顺序解决
第一种情况你做
(float * float) * Vector3
所以步骤:
1. (float * float) => single float * operation => result: float
2. (float * Vector3) => 3 float * operations => result: Vector3
在你做的那一秒
(float * Vector3) * float
所以步骤:
1. (float * Vector3) => 3 float * operations => result: Vector3
2. (Vector3 * float) => 3 float * operations => result: Vector3
在第三个等价物中你做
(Vector2 * float) * float
所以步骤
1. (Vector3 * float) => 3 float * operations => result: Vector3
2. (Vector3 * float) => 3 float * operations => result: Vector3
在第一种情况下,您只有 4 次 *
操作,而在其他两种情况下,您有 6 次操作。
Plus 在第二种和第三种情况下,您还实例化了一个额外的 Vector3
(第一个操作的结果),这也会消耗性能和内存。
要将 float
乘以 Vector3
,需要进行三次乘法运算。要将 float
乘以 float
,显然只需要一次乘法。
当你这样做时:
result = a * b * c;
这意味着:
result = (a * b) * c;
或更明确地说:
var tmp = a * b;
result = tmp * c;
假设 a
和 b
是浮点数,c
是向量。然后 a * b
执行一次乘法(float
乘以 float
)并得到 float
,然后第二个运算执行三次乘法(float
乘以 Vector3
).总共是四次乘法。
现在支持 a
是向量,b
和 c
是浮点数。然后 a * b
执行三次乘法(Vector3
乘以 float
)并得到 Vector3
,然后第二个运算再次执行三次乘法(Vector3
乘以 float
) 并产生 Vector3
。总共是六次乘法。
现在,可能 编译器可能能够弄清楚这些是等价的,并改为执行更便宜的四次乘法。但通常它并不容易看到这一点。通常它 不 完全等价。例如,舍入和溢出在很大程度上取决于您执行操作的方式,并且编译器不会弄乱它们,因为它可能会引入不准确,因此它信任您并简单地按照您告诉它的去做。
我看了a video Tarodev 的代码优化。在视频的最后,我很震惊,因为我看到操作顺序可以严重改变代码速度。虽然自己试过了,但是不明白怎么可能呢? Float x Float x Vector 如何比 Float x Vector x Float 或 Vector x Float x Float 快 2 倍?
我从 GitHub 安装了 Tarodev 的项目并自己尝试了。结果和视频一样。
编辑:
感谢所有这些很棒的回答,伙计们。在你回答之后,我明白了原因并感到愚蠢 :) 另外,我在 Orhtej2 的 建议
中添加了 video Repository and example code答案很简单,按照我的例子:
向量是由超过 1 个值组成的对象:例如Vector3 v = new Vector3(1,2,3)
如果您将 Vector3 的数字相乘,您将进行 3 次运算。
float n = 3.f
Vector3 v = new Vector3(1,2,3)
n*v
结果是 Vector3(1*n,2*n,3*n)
,所以一次乘法需要 3 次计算。
在第一种情况下按照您的示例进行 n*n*v
,因此 n*n + 3
计算 n^2 * v
的 1 次计算,总共 4 次计算。
在第二种情况下,你正在做 n * v * n
,这意味着 n*v
的 3 次计算 + v*n
的另外 3 次计算,总共 6 次计算。
Vector x Scalar1 x Scalar2 => (Vector x Scalar1) x Scalar2 =>
VectorTemp x Scalar2 = EndResult
总而言之,我们必须通过向量运算对标量进行两次运算。意义 向量长度乘法的 2 倍。
对比
Scalar1 x Scalar2 x Vector = (Scalar1 x Scalar2) x Vector =>
Scalar3 x Vector = EndResult.
这里需要一个Vector x Scalar运算。 Scalar2 的 Scalar1 只是 multipication
所以我想这里的关键思想是如果操作是可交换的,从最便宜的开始。
首先
someVector3 * someFloat
和
someFloat * someVector3
基本上都是return
new Vector3(someVector3.x * someFloat, someVector3.y * someFloat, someVector3.z * someFloat)
所以基本上是 3 float * float
操作 + Vector3
然后顺序很重要,因为您的操作将按这样的顺序解决
第一种情况你做
(float * float) * Vector3
所以步骤:
1. (float * float) => single float * operation => result: float 2. (float * Vector3) => 3 float * operations => result: Vector3
在你做的那一秒
(float * Vector3) * float
所以步骤:
1. (float * Vector3) => 3 float * operations => result: Vector3 2. (Vector3 * float) => 3 float * operations => result: Vector3
在第三个等价物中你做
(Vector2 * float) * float
所以步骤
1. (Vector3 * float) => 3 float * operations => result: Vector3 2. (Vector3 * float) => 3 float * operations => result: Vector3
在第一种情况下,您只有 4 次 *
操作,而在其他两种情况下,您有 6 次操作。
Plus 在第二种和第三种情况下,您还实例化了一个额外的 Vector3
(第一个操作的结果),这也会消耗性能和内存。
要将 float
乘以 Vector3
,需要进行三次乘法运算。要将 float
乘以 float
,显然只需要一次乘法。
当你这样做时:
result = a * b * c;
这意味着:
result = (a * b) * c;
或更明确地说:
var tmp = a * b;
result = tmp * c;
假设 a
和 b
是浮点数,c
是向量。然后 a * b
执行一次乘法(float
乘以 float
)并得到 float
,然后第二个运算执行三次乘法(float
乘以 Vector3
).总共是四次乘法。
现在支持 a
是向量,b
和 c
是浮点数。然后 a * b
执行三次乘法(Vector3
乘以 float
)并得到 Vector3
,然后第二个运算再次执行三次乘法(Vector3
乘以 float
) 并产生 Vector3
。总共是六次乘法。
现在,可能 编译器可能能够弄清楚这些是等价的,并改为执行更便宜的四次乘法。但通常它并不容易看到这一点。通常它 不 完全等价。例如,舍入和溢出在很大程度上取决于您执行操作的方式,并且编译器不会弄乱它们,因为它可能会引入不准确,因此它信任您并简单地按照您告诉它的去做。