如何优化RGB到HSL转换函数的执行时间?

3
我创建了这个函数来将RGB颜色转换为HSL颜色。它运行得非常完美。
但是我需要让它运行得更快,因为它用于替换画布上的颜色,我需要减少替换时间。由于图像包含360k像素(600x600px),任何提高速度的方式都是必要的。
这是我的当前实现:
/**
 * Convert RGB Color to HSL Color
 * @param {{R: integer, G: integer, B: integer}} rgb
 * @returns {{H: number, S: number, L: number}}
 */
Colorize.prototype.rgbToHsl = function(rgb) {
    var R = rgb.R/255;
    var G = rgb.G/255;
    var B = rgb.B/255;
    var Cmax = Math.max(R,G,B);
    var Cmin = Math.min(R,G,B);
    var delta = Cmax - Cmin;
    var L = (Cmax + Cmin) / 2;
    var S = 0;
    var H = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2*L) - 1));

        switch (Cmax) {
            case R:
                H = ((G - B) / delta) % 6;
                break;
            case G:
                H = ((B - R) / delta) + 2;
                break;
            case B:
                H = ((R - G) / delta) + 4;
                break;
        }
        H *= 60;
    }

    // Convert negative angles from Hue
    while (H < 0) {
        H += 360;
    }

    return {
        H: H,
        S: S,
        L: L
    };
};

1
一种优化方法是删除那个 while 循环,改为 if (H < 0) H = -(H % 360)。如果我没记错的话,这将给你相同的结果。 - Mike Cluck
3个回答

7

简述

  • 在计算之前定义所有内容
  • Switch语句会影响性能
  • 循环也会影响性能
  • 使用闭包编译器进行自动优化
  • 记忆化(这个在基准测试中不可用,因为它一次只使用一种颜色)
  • Math.maxMath.min中比较对(就我所看到的而言,对于较大的数字,if-else效果更好)

基准测试

基准测试相当基础; 我每次都生成一个随机RGB颜色并将其用于测试套件。

对于所有转换器实现,使用相同的颜色。

我目前正在使用一台快速的计算机,因此您的数字可能会有所不同。

此时很难进一步优化,因为性能取决于数据。

优化

在最开始定义结果值的对象。预先分配对象的内存可以提高性能。

var res = {
    H: 0,
    S: 0,
    L: L
}
// ...
return res;

不使用switch会带来简单的性能提升。
if (delta !== 0) {
    S = delta / (1 - Math.abs((2 * L) - 1));

    if (Cmax === R) {
        H = ((G - B) / delta) % 6;
    } else if (Cmax === G) {
        H = ((B - R) / delta) + 2;
    } else if (Cmax === B) {
        H = ((R - G) / delta) + 4;
    }
    H *= 60;
}

使用以下方法可以轻松删除while循环:

if (H < 0) {
    remainder = H % 360;

    if (remainder !== 0) {
        H = remainder + 360;
    }
}

我还使用Closure Compiler来删除代码中的冗余操作。

你应该记忆化结果!

考虑重构函数,使用三个参数,这样可以拥有一个多维哈希映射用于缓存记忆。或者你可以尝试使用WeakMap,但是这种解决方案的性能是未知的。

请参阅文章Faster JavaScript Memoization For Improved Application Performance

结果

Node.js

最好的一个是 rgbToHslOptimizedClosure
node -v
v6.5.0

rgbToHsl x 16,468,872 ops/sec ±1.64% (85 runs sampled)
rgbToHsl_ x 15,795,460 ops/sec ±1.28% (84 runs sampled)
rgbToHslIfElse x 16,091,606 ops/sec ±1.41% (85 runs sampled)
rgbToHslOptimized x 22,147,449 ops/sec ±1.96% (81 runs sampled)
rgbToHslOptimizedClosure x 46,493,753 ops/sec ±1.55% (85 runs sampled)
rgbToHslOptimizedIfElse x 21,825,646 ops/sec ±2.93% (85 runs sampled)
rgbToHslOptimizedClosureIfElse x 38,346,283 ops/sec ±9.02% (73 runs sampled)
rgbToHslOptimizedIfElseConstant x 30,461,643 ops/sec ±2.68% (81 runs sampled)
rgbToHslOptimizedIfElseConstantClosure x 40,625,530 ops/sec ±2.70% (73 runs sampled)

Fastest is rgbToHslOptimizedClosure
Slowest is rgbToHsl_

浏览器

Chrome Version 55.0.2883.95 (64-bit)

rgbToHsl x 18,456,955 ops/sec ±0.78% (62 runs sampled)
rgbToHsl_ x 16,629,042 ops/sec ±2.34% (63 runs sampled)
rgbToHslIfElse x 17,177,059 ops/sec ±3.85% (59 runs sampled)
rgbToHslOptimized x 27,552,325 ops/sec ±0.95% (62 runs sampled)
rgbToHslOptimizedClosure x 47,659,771 ops/sec ±3.24% (47 runs sampled)
rgbToHslOptimizedIfElse x 26,033,751 ops/sec ±2.63% (61 runs sampled)
rgbToHslOptimizedClosureIfElse x 43,430,875 ops/sec ±3.55% (59 runs sampled)
rgbToHslOptimizedIfElseConstant x 33,696,558 ops/sec ±3.97% (58 runs sampled)
rgbToHslOptimizedIfElseConstantClosure x 44,529,209 ops/sec ±3.56% (60 runs sampled)

Fastest is rgbToHslOptimizedClosure
Slowest is rgbToHsl_

自行运行基准测试

请注意,浏览器会在一瞬间冻结。

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

var RGB = { R: getRandomInt(0, 255), G: getRandomInt(0, 255), B: getRandomInt(0, 255) }

// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c

/**
 * Converts an RGB color value to HSL. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes r, g, and b are contained in the set [0, 255] and
 * returns h, s, and l in the set [0, 1].
 *
 * @param   {number}  r       The red color value
 * @param   {number}  g       The green color value
 * @param   {number}  b       The blue color value
 * @return  {Array}           The HSL representation
 */
function rgbToHsl_(r, g, b) {
    r /= 255, g /= 255, b /= 255;
    var max = Math.max(r, g, b), min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2;

    if (max == min) {
        h = s = 0; // achromatic
    } else {
        var d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
            case r:
                h = (g - b) / d + (g < b ? 6 : 0);
                break;
            case g:
                h = (b - r) / d + 2;
                break;
            case b:
                h = (r - g) / d + 4;
                break;
        }
        h /= 6;
    }

    return [ h, s, l ];
}

function rgbToHsl(rgb) {
    var R = rgb.R / 255;
    var G = rgb.G / 255;
    var B = rgb.B / 255;
    var Cmax = Math.max(R, G, B);
    var Cmin = Math.min(R, G, B);
    var delta = Cmax - Cmin;
    var L = (Cmax + Cmin) / 2;
    var S = 0;
    var H = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2 * L) - 1));

        switch (Cmax) {
            case R:
                H = ((G - B) / delta) % 6;
                break;
            case G:
                H = ((B - R) / delta) + 2;
                break;
            case B:
                H = ((R - G) / delta) + 4;
                break;
        }
        H *= 60;
    }

    // Convert negative angles from Hue
    while (H < 0) {
        H += 360;
    }

    return {
        H: H,
        S: S,
        L: L
    };
};

function rgbToHslIfElse(rgb) {
    var R = rgb.R / 255;
    var G = rgb.G / 255;
    var B = rgb.B / 255;
    var Cmax = Math.max(R, G, B);
    var Cmin = Math.min(R, G, B);
    var delta = Cmax - Cmin;
    var L = (Cmax + Cmin) / 2;
    var S = 0;
    var H = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2 * L) - 1));

        if (Cmax === R) {
            H = ((G - B) / delta) % 6;
        } else if (Cmax === G) {
            H = ((B - R) / delta) + 2;
        } else if (Cmax === B) {
            H = ((R - G) / delta) + 4;
        }
        H *= 60;
    }

    // Convert negative angles from Hue
    while (H < 0) {
        H += 360;
    }

    return {
        H: H,
        S: S,
        L: L
    };
};

function rgbToHslOptimized(rgb) {
    var R = rgb.R / 255;
    var G = rgb.G / 255;
    var B = rgb.B / 255;
    var Cmax = Math.max(Math.max(R, G), B);
    var Cmin = Math.min(Math.min(R, G), B);
    var delta = Cmax - Cmin;
    var S = 0;
    var H = 0;
    var L = (Cmax + Cmin) / 2;
    var res = {
        H: 0,
        S: 0,
        L: L
    }
    var remainder = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2 * L) - 1));

        switch (Cmax) {
            case R:
                H = ((G - B) / delta) % 6;
                break;
            case G:
                H = ((B - R) / delta) + 2;
                break;
            case B:
                H = ((R - G) / delta) + 4;
                break;
        }
        H *= 60;
    }

    if (H < 0) {
        remainder = H % 360;

        if (remainder !== 0) {
            H = remainder + 360;
        }
    }

    res.H = H;
    res.S = S;

    return res;
}

function rgbToHslOptimizedIfElse(rgb) {
    var R = rgb.R / 255;
    var G = rgb.G / 255;
    var B = rgb.B / 255;
    var Cmax = Math.max(Math.max(R, G), B);
    var Cmin = Math.min(Math.min(R, G), B);
    var delta = Cmax - Cmin;
    var S = 0;
    var H = 0;
    var L = (Cmax + Cmin) / 2;
    var res = {
        H: 0,
        S: 0,
        L: L
    }
    var remainder = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2 * L) - 1));

        if (Cmax === R) {
            H = ((G - B) / delta) % 6;
        } else if (Cmax === G) {
            H = ((B - R) / delta) + 2;
        } else if (Cmax === B) {
            H = ((R - G) / delta) + 4;
        }
        H *= 60;
    }

    if (H < 0) {
        remainder = H % 360;

        if (remainder !== 0) {
            H = remainder + 360;
        }
    }

    res.H = H;
    res.S = S;

    return res;
}

function rgbToHslOptimizedIfElseConstant(rgb) {
    var R = rgb.R * 0.00392156862745;
    var G = rgb.G * 0.00392156862745;
    var B = rgb.B * 0.00392156862745;
    var Cmax = Math.max(Math.max(R, G), B);
    var Cmin = Math.min(Math.min(R, G), B);
    var delta = Cmax - Cmin;
    var S = 0;
    var H = 0;
    var L = (Cmax + Cmin) * 0.5;
    var res = {
        H: 0,
        S: 0,
        L: L
    }
    var remainder = 0;

    if (delta !== 0) {
        S = delta / (1 - Math.abs((2 * L) - 1));

        if (Cmax === R) {
            H = ((G - B) / delta) % 6;
        } else if (Cmax === G) {
            H = ((B - R) / delta) + 2;
        } else if (Cmax === B) {
            H = ((R - G) / delta) + 4;
        }
        H *= 60;
    }

    if (H < 0) {
        remainder = H % 360;

        if (remainder !== 0) {
            H = remainder + 360;
        }
    }

    res.H = H;
    res.S = S;

    return res;
}

function rgbToHslOptimizedIfElseConstantClosure(c) {
    var a = .00392156862745 * c.h, e = .00392156862745 * c.f, f = .00392156862745 * c.c, g = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), h = g - d, b = c = 0, k = (g + d) / 2, d = {
        a: 0,
        b: 0,
        g: k
    };
    0 !== h && (c = h / (1 - Math.abs(2 * k - 1)), g === a ? b = (e - f) / h % 6 : g === e ? b = (f - a) / h + 2 : g === f && (b = (a - e) / h + 4), b *= 60);
    0 > b && (a = b % 360, 0 !== a && (b = a + 360));
    d.a = b;
    d.b = c;
    return d;
};

function rgbToHslOptimizedClosure(c) {
    var a = c.f / 255, e = c.b / 255, f = c.a / 255, k = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), g = k - d, b = c = 0, l = (k + d) / 2, d = {
        c: 0,
        g: 0,
        h: l
    };
    if (0 !== g) {
        c = g / (1 - Math.abs(2 * l - 1));
        switch (k) {
            case a:
                b = (e - f) / g % 6;
                break;
            case e:
                b = (f - a) / g + 2;
                break;
            case f:
                b = (a - e) / g + 4;
        }
        b *= 60;
    }
    0 > b && (a = b % 360, 0 !== a && (b = a + 360));
    d.c = b;
    d.g = c;
    return d;
}

function rgbToHslOptimizedClosureIfElse(c) {
    var a = c.f / 255, e = c.b / 255, f = c.a / 255, g = Math.max(Math.max(a, e), f), d = Math.min(Math.min(a, e), f), h = g - d, b = c = 0, l = (g + d) / 2, d = {
        c: 0,
        g: 0,
        h: l
    };
    0 !== h && (c = h / (1 - Math.abs(2 * l - 1)), g === a ? b = (e - f) / h % 6 : g === e ? b = (f - a) / h + 2 : g === f && (b = (a - e) / h + 4), b *= 60);
    0 > b && (a = b % 360, 0 !== a && (b = a + 360));
    d.c = b;
    d.g = c;
    return d;
}

new Benchmark.Suite()
    .add('rgbToHsl', function () {
        rgbToHsl(RGB);
    })
    .add('rgbToHsl_', function () {
        rgbToHsl_(RGB.R, RGB.G, RGB.B);
    })
    .add('rgbToHslIfElse', function () {
        rgbToHslIfElse(RGB);
    })
    .add('rgbToHslOptimized', function () {
        rgbToHslOptimized(RGB);
    })
    .add('rgbToHslOptimizedClosure', function () {
        rgbToHslOptimizedClosure(RGB);
    })
    .add('rgbToHslOptimizedIfElse', function () {
        rgbToHslOptimizedIfElse(RGB);
    })
    .add('rgbToHslOptimizedClosureIfElse', function () {
        rgbToHslOptimizedClosureIfElse(RGB);
    })
    .add('rgbToHslOptimizedIfElseConstant', function () {
        rgbToHslOptimizedIfElseConstant(RGB);
    })
    .add('rgbToHslOptimizedIfElseConstantClosure', function () {
        rgbToHslOptimizedIfElseConstantClosure(RGB);
    })
    // add listeners
    .on('cycle', function (event) {
        console.log(String(event.target));
    })
    .on('complete', function () {
        console.log('Fastest is ' + this.filter('fastest').map('name'));
        console.log('Slowest is ' + this.filter('slowest').map('name'));
    })
    // run async
    .run({ 'async': false });
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.rawgit.com/bestiejs/benchmark.js/master/benchmark.js"></script>


非常好的答案。有很大的机会赢得悬赏! :) 我很快就会进行测试!谢谢 - Elias Soares
你尝试过用x * 0.00392156862745替换x / 255吗? - ConnorsFan
@ConnorsFan 是的,对我来说它产生了一些模棱两可的结果。我会更新基准测试。 - halfzebra
这绝对是最好的答案。虽然我没有完全使用你的代码,但是你给了我一个方向。我意识到,对于我的操作(着色图像),我不需要原始的H和S,只需要HSL中的L部分,因此我将函数从rgbToHsl减少到rgbToL。经过所有的优化,这就是最终结果: `function(R, G, B) { var L; if (R < G && R > B) { L = G + B; } else if (G < R && G > B) { L = R + B; } else { L = G + R; } return L / 510; }` - Elias Soares
2
另外一个使基准测试更加真实的技巧: 不要使用随机RGB,而是使用所有可能的RGB,因为我发现某些特定颜色比其他颜色更快。 - Elias Soares
@EliasSoares 很高兴能帮忙! - halfzebra

4

尽可能避免除法。您可以通过重新调整相关变量和常量来消除其中一些。

您也可以通过使用反演的查找表来避免除法。

我认为 switch case 不太有效率。我建议用三层嵌套的 if 语句替换 max/min/switch,其中比较 RGB 组件,最终得出 6 种可能的排序,并对每个应用特定的处理。


我已经用if-elseif-else的两个代码块替换了min/max调用,执行时间大大缩短。我将尝试避免使用switch,并发布结果。 - Elias Soares

-1

这个答案不适用:
  • 我需要精度
  • 我需要HSL来进行转换。
- Elias Soares

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