并行工作
线程不一定是并行化代码的唯一方法,cpus 通常具有指令集,例如 SIMD,它允许您一次对多个数字计算相同的算术。 GPU 采用了这个想法并与之一起运行,允许您在数百到数千个数字上并行运行相同的函数。我不知道如何在 java 中做到这一点,但我确信通过谷歌搜索可以找到一种有效的方法。
算法 - 做更少的工作
是否可以减少需要调用函数的时间?每帧调用任何函数一百万次都会受到伤害。除非每个函数调用的开销都得到管理(内联它、重用堆栈帧、尽可能缓存结果),否则你会希望做更少的工作。
可能的选择是:
- 缩小游戏的窗口/分辨率。
- 使用不同的表示。当像素是 HSV 而不是 RGB 时,您是否正在做很多更容易做的操作?然后仅在您即将渲染像素时转换为 RGB。
- 为每个像素使用有限数量的颜色。这样一来,您就可以提前计算出可能的色调,并且只需查找即可,而不是函数调用。
- 尽可能少地着色。也许有一些用户界面是有色的,不应该是。也许灯光效果只能传播这么远。
- 作为最后的手段,将着色设为默认值。如果对像素进行如此多的着色,那么“不着色”的发生可能会少得多,并且您可以通过这样做获得更好的性能。
性能 - (微)优化代码
如果您可以满足于“近似色调”this SO answer 会给出一个像素的亮度 (lum) 近似值,该像素的计算成本应该更低。 (来自链接的公式是 Y = 0.33 R + 0.5 G + 0.16 B,可以写成 Y = (R+R+B+G+G+G)/6。)
下一步是衡量您的代码(配置文件是谷歌搜索的一个很好的术语)并查看占用最多资源的内容。很可能这里不是这个函数,而是另一段代码。或者等待纹理加载。
从这一点开始,我们将假设问题中提供的功能占用的时间最多。让我们看看它在做什么。我没有你的其余代码,所以我无法对其进行基准测试,但我可以编译它并查看生成的字节码。在包含函数的类上使用 javap 我得到以下内容(字节码已被剪切,有重复)。
public static int tintABGRPixel(int, Color);
Code:
0: iload_0
1: bipush 16
3: ishr
4: sipush 255
7: iand
8: i2d
9: ldc2_w #2 // double 0.2126d
12: dmul
13: iload_0
...
37: dadd
38: ldc2_w #8 // double 255.0d
41: ddiv
42: dstore_2
43: iload_0
44: bipush 24
46: ishr
47: sipush 255
50: iand
51: bipush 24
53: ishl
54: aload_1
55: pop
56: invokestatic #10 // Method Color.getBlue:()I
59: i2d
60: dload_2
61: dmul
62: d2i
63: sipush 255
66: iand
67: ior
68: aload_1
69: pop
...
102: ireturn
一开始这看起来很吓人,但 java 字节码很好,因为您可以将每一行(或指令)与函数中的某个点相匹配。它没有做任何疯狂的事情,比如重写或矢量化或任何使其无法识别的事情。
查看更改是否有所改进的一般方法是测量前后的代码。有了这些知识,您就可以决定是否值得保留更改。一旦性能足够好,就停止。
我们的穷人剖析是查看每条指令,并查看(根据在线资料,平均而言)它的成本是多少。这有点幼稚,因为每条指令执行所需的时间可能取决于许多因素,例如运行它的硬件、计算机上的软件版本以及它周围的指令。
我没有每条指令的时间成本的完整列表,因此我将采用一些启发式方法。
- 整数运算比浮点运算快。
- 常量比本地内存快,比全局内存快。
- 2 的幂可以实现强大的优化。
我盯着字节码看了一会儿,我只注意到从第 8 到 42 行有很多浮点运算。这部分代码计算出 lum (亮度)。除此之外,没有什么特别突出的,所以让我们用我们的第一个启发式来重写代码。如果你不关心解释,我会在最后提供最终代码。
让我们只考虑函数结束时蓝色(我们将标记为 B)将是什么。这些更改也将适用于红色和绿色,但为简洁起见,我们将忽略它们。
double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
(pixelColor>>8 & 0xff) * 0.7152 +
(pixelColor & 0xff) * 0.0722) / 255;
...
... | ((int)(tintColor.getBlue()*lum) & 0xff) | ...
这可以重写为
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
双a = 0.2126,b = 0.7152,c = 0.0722;
双亮度 = (ax + by + c*z) / 255;
int B = (int)(tintColor.getBlue()*lum) & 0xff;
我们不想做那么多浮点运算,所以让我们做一些因式分解。这个想法是0.2126可以写成2126/10000。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
double temp = (double)(tintColor.getBlue() * top) / 10000 / 255;
int B = (int)temp & 0xff;
所以现在我们执行三个整数乘法 (imul) 而不是三个 dmul。成本是一个额外的浮动除法,仅此一项可能不值得。但是我们可以通过结合两个连续的划分来解决这个问题。我们还可以通过将强制转换和除法移动到一行来设置代码以进行更多优化。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z);
int temp = (int)((double)(tintColor.getBlue()*top) / 2550000);
int B = temp & 0xff;
这可能是一个停下来的好地方。但是,如果您需要从这个函数中挤出一点点更多的性能,我们可以优化除以一个常量并将一个 double 转换为一个 int(我认为这是两个昂贵的操作)到一个乘法(通过一个 long)和一个换班。
int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
int Btemp = (int)(( * top * 1766117501L) >> 52);
int B = temp & 0xff;
当我用 clang 编译代码的 c++ 版本时,魔术数字是两个。我无法解释如何产生这种魔法,但就我测试过的 x、y、z 和 tintColor.getBlue() 的几个值而言,它是有效的。在测试时,我假设所有值都在 0 到 256 的范围内,我只尝试了几个示例。
最终代码如下。请注意,这没有经过很好的测试,并且可能有我错过的边缘情况,所以如果有任何错误,请告诉我。希望它足够快。
public static int tintABGRPixel(int pixelColor, Color tintColor) {
//Calculate the luminance. The decimal values are pre-determined.
int x = pixelColor>>16 & 0xff, y = pixelColor>>8 & 0xff, z = pixelColor & 0xff;
int top = 2126*x + 7252*y + 722*z;
int Btemp = (int)((tintColor.getBlue() * top * 1766117501L) >> 52);
int Gtemp = (int)((tintColor.getGreen() * top * 1766117501L) >> 52);
int Rtemp = (int)((tintColor.getRed() * top * 1766117501L) >> 52);
//Calculate the new tinted color of the pixel and return it.
return ((pixelColor>>24 & 0xff) << 24) | Btemp & 0xff | (Gtemp & 0xff) << 8 | (Rtemp & 0xff) << 16;
}