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

How to Optimize execution time for RGB to HSL conversion function?

我创建了这个函数来将 RGB 颜色转换为 HSL 颜色。效果很好。

但我需要让它 运行 更快,因为它用于替换 canvas 上的颜色,我需要减少替换时间。由于图像包含 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
    };
};

一定要避免分裂。您可能可以通过重新缩放相关变量和常量来消除一些。

您还可以通过使用逆查找-table 来避免除法。

我认为 switch case 效率不高。我建议通过使用三重嵌套 if 的单个讨论来替换 max/min/switch,在其中比较 RGB 组件,以 6 种可能的顺序结束并对每个进行临时处理。

tl;博士

  • 在计算之前定义一切
  • 开关不利于性能
  • 循环不好
  • 使用 Closure Compiler 进行自动优化
  • 记住! (这个在基准测试中不可用,因为它当时只使用一种颜色)
  • 比较 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>

改用粗略的近似值:

third = Math.PI * 2 / 3;
ctx.fillStyle = 'rgb('+ [
    127 + 127 * Math.cos(time - third),
    127 + 127 * Math.cos(time),
    127 + 127 * Math.cos(time + third)
] +')';

基于: