如何最有效地修改R / G / B值?

3

我想在我的像素渲染系统中实现照明效果,搜了一下资料发现,为了显示RGB值更亮或更暗,我需要将每个红色、绿色和蓝色值乘以一个小于1的数字来显示它们变暗,并乘以一个大于1的数字来显示它们变亮。

所以我按照这种方法实现了它,但由于我必须对每个像素执行此操作,因此它真的拖慢了我的性能:

void PixelRenderer::applyLight(Uint32& color){
    Uint32 alpha = color >> 24;
    alpha << 24;
    alpha >> 24;

    Uint32 red = color >> 16;
    red = red << 24;
    red = red >> 24;

    Uint32 green = color >> 8;
    green = green << 24;
    green = green >> 24;

    Uint32 blue = color;
    blue = blue << 24;
    blue = blue >> 24;

    red = red * 0.5;
    green = green * 0.5;
    blue = blue * 0.5;
    color = alpha << 24 | red << 16 | green << 8 | blue;
}

有没有关于如何提高速度的想法或例子?

2
@Styxs:你确定这段代码导致了某些东西变慢吗?你的代码还做了什么其他事情?这个函数在整体性能分析中有多大的峰值? - Mats Petersson
4
(1) 使用位掩码代替移位操作。 (2) 对于大于1的值,这种方法将不起作用,你需要进行数值夹紧(clamp)。 (3) 所有基于像素的操作,在像素数量足够大的情况下都会变得缓慢。 - Jongware
3
哎呀,你正试图通过将double作为参数将整数除以二?难怪运行速度很慢。除了位掩码与移位操作之外的所有内容,都不及整型向双精度浮点型转换和双精度浮点型向整型转换所耗费的时间(特别是由于愚蠢的“朝零方向舍入”的语义要求在x87上进行大量操作)。 - Matteo Italia
3
使用比例整数(255=1,0=0),并进行整数运算。 - Matteo Italia
2
@MatteoItalia 我以前没有听说过这个,但我会查阅一些资料,谢谢。而且,我尝试了除以2,而不是乘以0.5,这极大地提高了性能。 - heap_trouble
显示剩余12条评论
5个回答

3
尝试这个:(编辑:事实证明,这只是一种可读性的改进,但请继续阅读以获取更多见解。)
void PixelRenderer::applyLight(Uint32& color)
{
    Uint32 alpha = color >> 24;
    Uint32 red = (color >> 16) & 0xff;
    Uint32 green = (color >> 8) & 0xff;
    Uint32 blue = color & 0xff;
    red = red * 0.5;
    green = green * 0.5;
    blue = blue * 0.5;
    color = alpha << 24 | red << 16 | green << 8 | blue;
}

话虽如此,您应该明白,在使用通用处理器(例如您计算机的CPU)执行此类操作时,速度极慢。这就是为什么硬件加速的图形卡被发明出来的原因。
编辑
如果您坚持以这种方式操作,那么您可能不得不采取一些技巧来提高效率。在处理8位通道值时经常使用的一种技巧是查找表。使用查找表时,您预先计算一个包含256个值的数组,其中数组的索引是通道值,该索引中的值是将该通道值乘以该浮点数的预计算结果。然后,在转换图像时,您只需使用通道值查找数组条目,而不是执行实际的浮点乘法。这样会快得多。(但仍然没有编程专用的、大规模并行的硬件来为您完成这项工作快。)
编辑
正如其他人已经指出的那样,如果您不打算对alpha通道进行操作,那么您不需要提取它,然后再应用它,您可以将其保留不变。因此,您只需执行color = (color & 0xff000000) | red << 16 | green << 8 | blue;

1
@hvd 嗯,实际上看:他说0.5只是一个例子。所以他需要一个通用算法,这意味着我的答案可能是迄今为止最好的。 - Mike Nakis
2
在使用GCC 4.9.2进行测试并使用“-march=native -O3”进行编译时,我得到了与您的问题中的代码和此答案中的代码相同的机器代码。 - user743382
1
@hvd 实际上,即使需要在每次对图像进行操作之前计算查找表,它也可以非常好。如果您要处理一个1920x1050的图像,则需要进行256次浮点乘法,而不是六百万次。 - Mike Nakis
1
@MikeNakis 是的,但这假设整个图像应该由相同的因素修改。OP正在实现渲染系统中的照明,这表明我认为这种假设不成立。 - user743382
1
没有必要“向下和向后移位”来执行乘法。这一切都可以通过掩码实现。请记住,移位乘法和移位回来与乘法相同(除了小数部分和溢出)。 - Persixty
显示剩余5条评论

3

在现代处理器上,像这样的移位和掩码通常非常快速。我建议您考虑以下几点:

  1. 遵循优化的第一法则-对代码进行分析。您可以通过调用该方法数百万次并计时来实现简单的分析。您的计算速度慢吗?还是其他原因导致的?什么是缓慢的?尝试省略方法的一部分-速度会提高吗?
  2. 确保此函数已声明为内联函数(并确保已实际将其内联)。函数调用开销将大大超过像素操作(特别是如果它是虚拟的)。
  3. 考虑将您的方法声明为Uint32 PixelRenderer::applyLight(Uint32 color)并返回修改后的值,这可能有助于避免一些间接引用,并为编译器提供额外的优化机会。
  4. 避免浮点转整数的转换,它们可能非常昂贵。如果普通的整数除法不足够,请考虑使用定点数学。

最后,查看汇编以查看编译器生成了什么(启用优化)。是否有任何分支或转换?您的方法是否已实际内联?


2
为了保留前景中的alpha值,请使用以下方法:
(color>>1)&0x7F7F7F | (color&0xFF000000)

(对Wimmel在评论中提供的建议进行微调。)
我认为这里的“学习曲线”是您使用Shift和Shift返回来屏蔽位。您应该使用&和掩码值。
对于更一般的解决方案(其中0.0<=factor<=1.0):
void PixelRenderer::applyLight(Uint32& color, double factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  (color&0x00FF0000)*factor;
    Uint32 green= (color&0x0000FF00)*factor;
    Uint32 blue=(color&0x000000FF)*factor;

   color=alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

请注意,在执行乘法之前没有必要将组件移动到低位。

最终,您可能会发现瓶颈是浮点数转换和算术运算。

为了减少这种情况,您应该考虑以下两种方法之一:

  1. 将其减少到比例因子中,例如0-256范围内。

  2. 预计算factor*component作为256个元素数组,并“选择”其中的组件。

我建议使用257个范围,因为您可以按如下方式获得因子:

对于更一般的解决方案(其中0<=factor<=256):

void PixelRenderer::applyLight(Uint32& color, Uint32 factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  ((color&0x00FF0000)*factor)>>8;
    Uint32 green= ((color&0x0000FF00)*factor)>>8;
    Uint32 blue=((color&0x000000FF)*factor)>>8;

    color=alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

这是一个可运行的程序,展示了第一个例子:
#include <stdio.h>
#include <inttypes.h>

typedef uint32_t Uint32;

Uint32 make(Uint32 alpha,Uint32 red,Uint32 green,Uint32 blue){
    return (alpha<<24)|(red<<16)|(green<<8)|blue;
}

void output(Uint32 color){
    printf("alpha=%"PRIu32" red=%"PRIu32" green=%"PRIu32" blue=%"PRIu32"\n",(color>>24),(color&0xFF0000)>>16,(color&0xFF00)>>8,color&0xFF);
}

Uint32 applyLight(Uint32 color, double factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  (color&0x00FF0000)*factor;
    Uint32 green= (color&0x0000FF00)*factor;
    Uint32 blue=(color&0x000000FF)*factor;

    return alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

int main(void) {
    Uint32 color1=make(156,100,50,20);
    Uint32 result1=applyLight(color1,0.9);
    output(result1);

    Uint32 color2=make(255,255,255,255);
    Uint32 result2=applyLight(color2,0.1);
    output(result2);

    Uint32 color3=make(78,220,200,100);
    Uint32 result3=applyLight(color3,0.05);
    output(result3);

    return 0;
}

预期输出是:
alpha=156 red=90 green=45 blue=18
alpha=255 red=25 green=25 blue=25
alpha=78 red=11 green=10 blue=5

@Wimmel:感谢您的修复。 - Persixty
这可能很有用,但是OP说* 0.5只是一个例子。 - user743382
@Styxs:那么你使用的factor值是多少?如果需要帮助集成代码,请尝试发布另一个问题。请提供一个MCVE。我们现在正在进行一次对话,我们都可以看到的唯一代码是有效的。 - Persixty
1
没事了,我的参数仍然是无符号整数,而不是双精度浮点数。但无论如何,使用双精度浮点数进行乘法似乎非常慢。所以我将使用您另一个示例,其中使用整数作为因子。感谢您抽出时间。 - heap_trouble
1
请注意,对于因子大于1.0(使图像变亮)的情况,这种方法不可靠。您需要将值剪切到255(这将引入一些分支,会减慢速度)。 - Bids
显示剩余3条评论

2
我看到其他人没有提到的一件事是将代码并行化。至少有两种方法可以实现:SIMD指令和多线程。 SIMD指令(如SSE、AVX等)同时对多个数据执行相同的数学运算。因此,您可以例如在一条指令中将像素的红色、绿色、蓝色和alpha乘以相同的值:
vec4 lightValue = vec4(0.5, 0.5, 0.5, 1.0);
vec4 result = vec_Mult(inputPixel, lightValue);

这相当于:
lightValue.red = 0.5;
lightValue.green = 0.5;
lightValue.blue = 0.5;
lightValue.alpha = 1.0;

result.red = inputPixel.red * lightValue.red;
result.green = inputPixel.green * lightValue.green;
result.blue = inputPixel.blue * lightValue.blue;
result.alpha = inputPixel.alpha * lightValue.alpha;

您还可以将图像切成瓦片,并使用在多个核心上运行的线程同时对几个瓦片执行亮度操作。如果您使用的是C++11,可以使用std::thread启动多个线程。否则,您的操作系统可能具有线程功能,例如WinThreadsGrand Central Dispatchpthreadsboost threadsThreading Building Blocks等。

您可以结合以上两种方法,编写多线程代码,一次处理整个像素。

如果您想进一步处理,可以使用OpenGLOpenCLDirectXMetalMantleCUDA或其他GPGPU技术在机器的GPU上进行处理。GPU通常具有数百个核心,可以并行快速处理许多瓷砖,每个瓷砖一次处理整个像素(而不仅仅是通道)。
但是更好的选择可能是根本不写任何代码。极有可能已经有人完成了这项工作,您可以利用它。例如,在MacOS上有CoreImageAccelerate框架。在iOS上,您也有CoreImage,并且还有GPUImage。我相信在Windows、Linux和其他您可能使用的操作系统上也有类似的库。

我并不真正理解如何实现那个“SIMD”指令,或者甚至不知道那个例子是做什么的。 - heap_trouble
1
您不需要实现SIMD指令。您的处理器可能已经内置了类似的功能。对于我的示例有些含糊不清,我感到抱歉。您没有指定平台,而且SIMD指令不太可移植,所以我将其编写为伪代码。它所做的就是将lightValue(红色=0.5,绿色=0.5,蓝色=0.5,alpha = 1.0)乘以inputPixel。因此,它应该与您在示例中指定的内容类似。 - user1118321
我更新了答案以更好地解释,并提供了一些其他工具的链接。 - user1118321
是的,我可能会考虑用OpenGL重新编程,但我现在真的没有时间学习OpenGL。 - heap_trouble

1
  • 另一种不使用位移运算符的解决方案是将你的 32 位 uint 转换为一个 struct
  • 尝试将你的实现保留在 .h 包含文件中,以便可以进行内联
  • 如果你不想内联实现(参见上文),请修改你的 applyLight 方法以接受像素数组。对于这样一个小方法,方法调用开销可能很大
  • 在编译器上启用 "loop unroll" 优化,这将启用 SIMD 指令的使用

实现:

class brightness {
private:
    struct pixel { uint8_t b, g, r, a; };
    float factor;

    static inline void apply(uint8_t& p, float f) {
        p = max(min(int(p * f), 255),0);
    }

public:
    brightness(float factor) : factor(factor) { }

    void apply(uint32_t& color){
        pixel& p = (pixel&)color;

        apply(p.b, factor);
        apply(p.g, factor);
        apply(p.r, factor);
    }
};

使用查找表实现(在使用“循环展开”时速度较慢):
class brightness {

    struct pixel { uint8_t b, g, r, a; };

    uint8_t table[256];

public:
    brightness(float factor) {
        for(int i = 0; i < 256; i++)
            table[i] = max(min(int(i * factor), 255), 0);
    }

    void apply(uint32_t& color){
        pixel& p = (pixel&)color;

        p.b = table[p.b];
        p.g = table[p.g];
        p.r = table[p.r];
    }
};




// usage
brightness half_bright(0.5);
uint32_t pixel = 0xffffffff;
half_bright.apply(pixel);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接