在JavaScript中检查字符串是否为空(即仅包含空格)的最高性能方法?

时间:2013-06-09 13:30:52

标签: javascript performance

我需要编写一个测试函数,如果给定的字符串是“空白”,在某种意义上它只包含空白字符。空白字符如下:

'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
' ',
'\u0085',
'\u00A0',
'\u1680',
'\u180E',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000'

该函数将被调用很多次,因此它必须真正,真正高效。但是不应该占用太多内存(比如将每个字符映射到数组中的true / false)。到目前为止我尝试过的事情:

  • regexp - 性能不佳
  • 修剪并检查长度是否为0 - 性能不佳,还使用额外的内存来保存修剪后的字符串
  • 针对包含空格字符(if (!whitespaceCharactersMap[str[index]]) ...)的哈希集检查每个字符串字符 - 效果不错
  • 我目前的解决方案使用硬编码比较:

    function(str) {
        var length = str.length;
        if (!length) {
            return true;
        }
        for (var index = 0; index < length; index++)
        {
            var c = str[index];
            if (c === ' ')
            {
                // skip
            }
            else if (c > '\u000D' && c < '\u0085')
            {
                return false;
            }
            else if (c < '\u00A0')
            {
                if (c < '\u0009')
                {
                    return false;
                }
                else if (c > '\u0085')
                {
                    return false;
                }
            }
            else if (c > '\u00A0')
            {
                if (c < '\u2028')
                {
                    if (c < '\u180E')
                    {
                        if (c < '\u1680')
                        {
                            return false;
                        }
                        else if(c > '\u1680')
                        {
                            return false;
                        }
                    }
                    else if (c > '\u180E')
                    {
                        if (c < '\u2000')
                        {
                            return false;
                        }
                        else if (c > '\u200A')
                        {
                            return false;
                        }
                    }
                }
                else if (c > '\u2029')
                {
                    if (c < '\u205F')
                    {
                        if (c < '\u202F')
                        {
                            return false;
                        }
                        else if (c > '\u202F')
                        {
                            return false;
                        }
                    }
                    else if (c > '\u205F')
                    {
                        if (c < '\u3000')
                        {
                            return false;
                        }
                        else if (c > '\u3000')
                        {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }
    

这似乎比哈希集(在Chrome上测试)快50-100%。

有人看到或知道更多选项吗?

更新1

我会在这里回答一些评论:

  • 这不只是检查用户输入的空白​​。我必须解析必须单独处理空格的某些数据格式。
  • 值得优化。我之前已经对代码进行过分析。检查空字符串似乎是一个问题。而且,正如我们所看到的,方法之间的性能差异可能高达10倍,这绝对是值得的。
  • 一般来说,我发现这种“哈希集与正则表达式与交换与分支”的挑战非常有教育意义。
  • 我需要与浏览器以及node.js相同的功能。

现在,这是我对性能测试的看法:

http://jsperf.com/hash-with-comparisons/6

如果你们经常进行这些测试,我将不胜感激。

初步结论:

  • branchlessTest(a^9*a^10*a^11...)在Chrome和Firefox中非常快,但在Safari中则不然。从性能角度来看,可能是Node.js的最佳选择。
  • 在Chrom和Firefox上,switchTest也相当快,但是,在Safari和Opera中,最慢的是
  • re.test(str)的正则表达式在各处表现都很好,甚至在Opera中表现最快。
  • 哈希和分支显示几乎所有地方的结果几乎相同。比较也类似,通常表现最差(这可能是由于实施,检查' '应该是第一个)。

总结一下,对于我的情况,我会选择以下正则表达式版本:

var re = /[^\s]/;
return !re.test(str);

原因:

  • 无分支版本在Chrome和Firefox中很酷,但不太便携
  • 在Safari中切换太慢
  • regexps似乎无处不在,它们在代码中也非常紧凑

3 个答案:

答案 0 :(得分:7)

硬编码解决方案似乎是最好的,但我认为switch应该更快。这取决于JavaScript解释器处理这些的方式(大多数编译器非常有效地执行此操作),因此可能特定于浏览器(即,某些编译器速度较慢,而其他编译器速度较慢)。此外,我不确定JavaScript对UTF字符串的速度有多快,因此您可以尝试在比较值之前将字符转换为整数代码。

for (var index = 0; index < length; index++)
{
    var c = str.charCodeAt(index);
    switch (c) {
        case 0x0009: case 0x000A: case 0x000B: case 0x000C: case 0x000D: case 0x0020:
        case 0x0085: case 0x00A0: case 0x1680: case 0x180E: case 0x2000: case 0x2001:
        case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007:
        case 0x2008: case 0x2009: case 0x200A: case 0x2028: case 0x2029: case 0x202F:
        case 0x205F: case 0x3000: continue;
    }
    return false;
}

要考虑的另一件事是改变for

for (var index in str)
{
    ...
}

修改

您的jsPerf测试得到了一些修订,当前的版本可用here。我的代码在Chrome 26和27以及IE10中明显更快,但它也是Firefox 18中最慢的代码。

我在64位Linux 上的 Firefox 20.0上运行了同样的测试(我不知道如何让jsPerf保存这些),结果证明它是两个最快的之一(并列使用trimTest,两者都是大约11.8M ops / sec)。我还测试了WinXP上的 Firefox 20.0.1 ,但是在VirtualBox下(仍然在64位Linux下,这可能会产生重大影响),这给了switchTest 10M ops / sec, trimTest以7.3M ops / sec排在第二位。

所以,我猜测性能取决于浏览器版本和/或甚至可能在底层操作系统/硬件上(我认为上面的FF18测试是在Win上)。在任何情况下,要制作一个真正优化的版本,你必须制作许多版本,在所有浏览器,操作系统,架构上测试每个版本......你可以获得一个,然后在你的页面中包含最适合的版本对于访问者的浏览器,操作系统,架构,......我不确定哪种代码值得麻烦。

答案 1 :(得分:4)

由于分支比大多数其他操作昂贵得多,因此您希望将分支保持在最低限度。因此,您的if / else语句序列可能不是非常高效。一种主要使用数学的方法会快得多。例如:

在不使用任何分支的情况下执行相等性检查的一种方法是使用按位运算。一个例子是,检查a == b:

a ^ b == 0

由于两个相似位(即1 ^ 1或0 ^ 0)的xor为0,因此xor-two两个相等的值产生0.这很有用,因为它允许我们将0视为“真”值,并做更多的数学。想象一下,我们有一堆以这种方式表示的布尔变量:非零数字为假,零意味着为真。如果我们想问,“这些都是真的吗?”我们简单地将它们相乘。如果它们中的任何一个为真(等于零),则整个结果将为零。

因此,例如,代码看起来像这样:

function(str) {
    for (var i = 0; i < str.length; i++) {
        var c = str[i];
        if ((c ^ '\u0009') * (c ^ '\u000A') * (c ^ '\u000B') ... == 0)
            continue;
        return false;
    }
    return true;
}

主要原因是这样做比仅仅执行以下操作更具性能:

if ((c == '\u0009') || (c == '\u000A') || (c == '\u0008') ...)

是JavaScript具有短路布尔运算符,这意味着每次使用||运算符时,它不仅执行或运算,还会检查它是否可以证明该语句必须为true到目前为止,这是一种昂贵的分支操作。另一方面,数学方法不涉及分支,除了if语句本身,因此应该更快。

答案 2 :(得分:0)

这会在字符串的字符上创建并使用'hash'查找,如果它检测到非空格,则返回false:

var wsList=['\u0009','\u000A','\u000B','\u000C','\u000D',' ','\u0085','\u00A0','\u1680','\u180E','\u2000','\u2001','\u2002','\u2003','\u2004','\u2005','\u2006','\u2007','\u2008','\u2009','\u200A','\u2028','\u2029','\u202F','\u205F','\u3000'];
var ws=Object.create(null);
wsList.forEach(function(char){ws[char]=true});
function isWhitespace(txt){
    for(var i=0, l=txt.length; i<l; ++i){
        if(!ws[txt[i]])return false;
    }
    return true;
}

var test1=" \u1680 \u000B \u2002 \u2004";
isWhitespace(test1);
/*
true
*/
var test2=" _ . a ";
isWhitespace(test2);
/*
false
*/

不确定它的性能(还)。在对jsperf进行快速测试后,与使用/^\s*$/的RegExp相比,结果相当缓慢。


编辑:

您应该使用的解决方案似乎可能取决于您正在使用的数据的性质:数据主要是空白还是非空白?也主要是ascii范围文本?通过对常见的非空白字符范围使用范围检查(通过if),在最常见的空格上使用switch,然后使用哈希查找,您可以加快平均测试用例的速度其他一切。如果测试的大多数数据由最常见的字符组成(在0x0--0x7F之间),这可能会提高测试的平均性能。

也许像这样(if / switch / hash的混合)可以工作:

/*same setup as above with variable ws being a hash lookup*/
function isWhitespaceHybrid(txt){
    for(var i=0, l=txt.length; i<l; ++i){
        var cc=txt.charCodeAt(i)
        //above space, below DEL
        if(cc>0x20 && cc<0x7F)return false;
        //switch only the most common whitespace
        switch(cc){
            case 0x20:
            case 0x9:
            case 0xA:
            case 0xD:
            continue;
        }
        //everything else use a somewhat slow hash lookup (execute for non-ascii range text)
        if(!ws[txt[i]])return false;
    }
    return true;
}