将字符串转为整型还是将整型转为字符串:哪种更快?

5
我需要将一个有效整数的字符串与一个整数值进行比较。
对于String str和int integer,我的选项如下:
1. Integer.parseInt(str) == integer 2. str.equals(Integer.toString(integer))
哪一种方式更快,是否有更好的方法?
以下内容受atlaste答案中EqualsIntString启发。
 private static boolean checkEquality(final String string, final long value) {

    if (string == null) {
        return false;
    }
    final int length = string.length();
    if (length == 0) {
        return false;
    }

    long absValue;
    final byte minIndex;
    if (string.charAt(0) == '-') {
        if (value > 0) {
            return false;
        }
        absValue = -value;
        minIndex = 1;
    } else {
        if (value < 0) {
            return false;
        }
        absValue = value;
        if (string.charAt(0) == '+') {
            minIndex = 1;
        } else {
            minIndex = 0;
        }
    }

    for (int idx = length - 1; idx >= minIndex; idx--) {
        final byte rem = (byte) (absValue % 10);
        absValue /= 10;
        final int diff = string.charAt(idx) - rem - 48;
        if (diff != 0) {
            return false;
        }
    }

    return absValue == 0;
}

1
就错误易发性而言,我猜测是 str.equals(String.valueOf(integer)); - EpicPandaForce
1
“Integer.toString” 对我来说似乎在逻辑上更快,但我并没有实际的线索。尽管如此,我相信 JVM 会对这种任务进行优化,以至于根本没有任何区别。 - Luan Nico
@EpicPandaForce,你是指NumberFormatException吗? - lab bhattacharjee
好问题,int.Parse 能胜过 int.ToString 确实有点奇怪...让我做一些测试并记录下一些细节... - atlaste
4个回答

5

只有一种方法来了解: 测量。

一个快速的 JMH 基准测试显示,无论整数的大小如何,Integer.parseInt 都更快。但我们谈到的是大约 10 纳秒左右的时间差,在您的应用程序级别上不太可能产生太大的影响。

如果您百分之百确定您的字符串是有效的整数,您还可以手动解析它,这将更快(请参见下面代码中的 parseManualequalsIntString 方法)。使用哪种方法也取决于您是否希望值经常相等或经常不同(如果它们经常相等,则 parseManual 效果更好;如果您预计它们平均不同,则 equalsIntString 更快)。

因此,您的数据集的特征也很重要。

完整结果(得分以每个操作的纳秒为单位:越小越好)-(n) 列是正在比较的整数。第一部分比较相等值,第二部分比较不相等的值。

Benchmark                                       (n)  Mode  Samples   Score   Error  Units
c.a.p.SO30507506.manual                           1  avgt       10   6.579 ± 0.131  ns/op
c.a.p.SO30507506.manual                       12345  avgt       10  10.017 ± 0.401  ns/op
c.a.p.SO30507506.manual                   123456789  avgt       10  12.490 ± 0.243  ns/op
c.a.p.SO30507506.manualAtlaste                    1  avgt       10   7.914 ± 0.144  ns/op
c.a.p.SO30507506.manualAtlaste                12345  avgt       10  15.902 ± 0.593  ns/op
c.a.p.SO30507506.manualAtlaste            123456789  avgt       10  28.117 ± 0.463  ns/op
c.a.p.SO30507506.parse                            1  avgt       10   8.495 ± 0.325  ns/op
c.a.p.SO30507506.parse                        12345  avgt       10  21.614 ± 0.564  ns/op
c.a.p.SO30507506.parse                    123456789  avgt       10  34.692 ± 0.572  ns/op
c.a.p.SO30507506.stringEquals                     1  avgt       10  21.597 ± 0.594  ns/op
c.a.p.SO30507506.stringEquals                 12345  avgt       10  36.948 ± 1.144  ns/op
c.a.p.SO30507506.stringEquals             123456789  avgt       10  44.444 ± 1.011  ns/op

c.a.p.SO30507506.manual_unequal                   1  avgt       10   7.011 ± 0.149  ns/op
c.a.p.SO30507506.manual_unequal               12345  avgt       10  10.244 ± 0.350  ns/op
c.a.p.SO30507506.manual_unequal           123456789  avgt       10  13.135 ± 0.797  ns/op
c.a.p.SO30507506.manualAtlaste_unequal            1  avgt       10   4.328 ± 0.111  ns/op
c.a.p.SO30507506.manualAtlaste_unequal        12345  avgt       10   4.359 ± 0.115  ns/op
c.a.p.SO30507506.manualAtlaste_unequal    123456789  avgt       10   4.339 ± 0.103  ns/op
c.a.p.SO30507506.parse_unequal                    1  avgt       10   8.304 ± 0.251  ns/op
c.a.p.SO30507506.parse_unequal                12345  avgt       10  21.514 ± 0.405  ns/op
c.a.p.SO30507506.parse_unequal            123456789  avgt       10  35.257 ± 1.043  ns/op
c.a.p.SO30507506.stringEquals_unequal             1  avgt       10  19.060 ± 0.162  ns/op
c.a.p.SO30507506.stringEquals_unequal         12345  avgt       10  31.829 ± 0.427  ns/op
c.a.p.SO30507506.stringEquals_unequal     123456789  avgt       10  41.870 ± 0.252  ns/op

代码:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
public class SO30507506 {

  @Param({"1", "12345", "123456789"}) int n;
  int i;
  String s;

  @Setup public void setup() {
    i = n;
    s = String.valueOf(n);
  }

  @Benchmark public boolean parse() {
    return Integer.parseInt(s) == i;
  }

  @Benchmark public boolean stringEquals() {
    return s.equals(Integer.toString(i));
  }

  @Benchmark public boolean manual() {
    return parseManual(s) == i;
  }

  @Benchmark public boolean manualAtlaste() {
    return equalsIntString(i, s);
  }

  @Benchmark public boolean parse_unequal() {
    return Integer.parseInt(s) == i * 2;
  }

  @Benchmark public boolean stringEquals_unequal() {
    return s.equals(Integer.toString(i * 2));
  }

  @Benchmark public boolean manual_unequal() {
    return parseManual(s) == i * 2;
  }

  @Benchmark public boolean manualAtlaste_unequal() {
    return equalsIntString(i * 2, s);
  }

  private static int parseManual(String s) {
    int result = 0;
    int sign = s.charAt(0) == '-' ? -1 : 1;
    int startIndex = (s.charAt(0) >= '0' && s.charAt(0) <= '9') ? 0 : 1;
    for (int i = startIndex; i < s.length(); i++) {
      result *= 10;
      result += s.charAt(i) - '0';
    }
    return result * sign;
  }

  private static boolean equalsIntString(int value, String s) {
    if (s.isEmpty()) return false; // This is never good.
    if ((s.charAt(0) == '-' && value >= 0) || (s.charAt(0) != '-' && value < 0)) return false; // positive/negative check

    // Define the limit. This is basically the end of the string to check.
    int limit = 0;
    if (value < 0) {
      limit = 1;
      value = -value;
    }

    for (int i = s.length() - 1; i >= limit; --i) {
      char expected = (char) ('0' + (value % 10)); // the modulo will be optimized by the JIT because 10 is a constant
      value /= 10; // same story.

      if (s.charAt(i) != expected) return false;
    }
    return true;
  }
}

1
你真快啊。速度比我预期的要慢得多,但至少方向是正确的。国际化使解析变慢了很多。 - maaartinus
@assylias 我其实不确定JIT是否足够聪明,可以完全将其优化消除。毕竟,您正在使用在HotSpot中静态编译的字符串;我曾看到Java JIT在这方面做出了惊人的事情。 - atlaste
@atlaste 字符串s在编译时是未知的 - 它在运行时从n中的setup方法实例化,而n本身仅在运行时通过注释由JMH“注入”。我认为这段代码中提到的优化是不可能的(结果往往证明:数字越大,操作时间越长)。 - assylias
@assylias 好的,没问题。另外,你只检查相等性;对于(更长的)字符串来说,不相等可能更快计算。 - atlaste

1
有趣的问题。为了清晰起见,测试时可能会出现很多错误,因为Java处理字符串的方式可能会影响结果。因此,让我们从构建适当的测试开始。
构建您的测试:
具体来说:适当的测试不依赖于loadstring,因为它会影响内存分配。您需要使用动态构造的字符串来进行测试。
您的整数的10-log(例如字符串的长度)将影响测试结果。字符串越长,Integer.tryParse所需的时间就越长。如果字符串更长,则需要计算更多的div/mul并花费更长的时间。影响性能的另一个因素是“-”符号。如果您有无符号整数,则应考虑这一点。
基本上,测量意味着:
- 创建具有适当长度的字符串(取决于您的数据!!!)。更多字符串=更好 - 创建与字符串数组匹配(或不匹配)的失败/通过整数数组。 - 垃圾回收。 - 使用这两个数组进行测试。
请确保在测试期间为此创建一个巨大的数组,以便您的测试不会受到影响。同时,请确保您使用的整数/随机数具有与您的数据相同的特征...因此,我无法为您执行测试,所以我只能坚持理论。
字符串转整数相等
了解字符串转整数转换的工作原理很有帮助,因此让我们从一个直截了当的解决方案开始,逐步提高。我目前的笔记本电脑上没有Java,所以对于C#语法我感到抱歉 :-) 不过你应该很容易修复它...
public int ConvertStringToInt(string s)
{
    int val = 0;
    if (s[0] == '-') // 1
    {
        for (int i = 1; i < s.Length; ++i )
        {
            if (s[i] >= '0' && s[i] <= '9') // 2
            {
                throw new Exception();
            }
            val = val * 10 + s[i] - '0';
        }
        return -val;
    }
    else
    {
        for (int i = 0; i < s.Length; ++i)
        {
            if (s[i] >= '0' && s[i] <= '9')
            {
                throw new Exception();
            }
            val = val * 10 + s[i] - '0';
        }
        return val;
    }
}

如果您确定字符串中的数字永远不会为负数,当然可以省略条件1。此外,如果您确定该字符串始终是一个数字(这在我看来是暗示),则可以优化2。我通常使用算术溢出来生成大的无符号数字,这将从2中删除一个附加条件。最终代码如下:
public int ConvertStringToInt(string s)
{
    int val = 0;
    if (s[0] == '-')
    {
        for (int i = 1; i < s.Length; ++i )
        {
            val = val * 10 + s[i] - '0';
        }
        return -val;
    }
    else
    {
        for (int i = 0; i < s.Length; ++i)
        {
            val = val * 10 + s[i] - '0';
        }
        return val;
    }
}

下一步,您希望实现相等而不是转换。那么,我们能够评估多懒惰呢?嗯,我们需要解析几乎整个字符串才能进行检查。唯一确定的是,如果我们遇到一个“-”字符,我们也需要一个负整数。我最终得到了这个:
public bool EqualsStringInt(string s, int value)
{
    int val = 0;
    if (s[0] == '-')
    {
        if (value >= 0) { return false; } // otherwise we expected another char

        for (int i = 1; i < s.Length; ++i )
        {
            val = val * 10 + s[i] - '0'; // s[i] must be a char between '0'-'9' as implied by the question.
        }
        return (-val) == value;
    }
    else
    {
        if (value < 0) { return false; } // otherwise we expected another char

        for (int i = 0; i < s.Length; ++i)
        {
            val = val * 10 + s[i] - '0';
        }
        return val == value;
    }
}

整数和字符串的比较

我曾经在 C++ 中编写了一些将整数转换为字符串的代码,链接在这里:C++ performance challenge: integer to std::string conversion。如果你真正在寻找性能方面的好解决方案,这里也有一些值得考虑。

然而,仅仅检查相等比那个更容易。如果你仔细看算法,你会注意到:

  • 缓冲区过量分配。你不需要这样做。如果你不等待 GC 和/或使用静态字符串来启动进程,你的测试结果会出错!
  • 缓冲区重新分配。如果你按顺序填充了缓冲区,还需要翻转它。如果你不想等待 GC,这将影响测试结果!

这两个问题在长期运行中都应该耗费时间,并且都会影响你的测试结果。

此时,有趣的是你实际上并不需要完整的字符串 - 你只需要一个单独的字符。因此,我们就从这个角度来思考:

  • 如果符号不匹配,则相等性失败
  • 如果第一个字符不匹配,则相等性失败
  • 如果生成的所有字符都相同,则相等性成功。

或者,在代码中:

public bool EqualsIntString(int value, string s)
{
    if (s.Length == 0) { return false; } // This is never good.
    if ((s[0] == '-' && value >= 0) || (s[0] != '-' && value < 0)) { return false; } // positive/negative check

    // Define the limit. This is basically the end of the string to check.
    int limit = 0;
    if (value < 0) // 1
    {
        limit = 1;
        value = -value;
    }

    for (int i=s.Length-1; i>=limit; --i)
    {
        char expected = (char)('0' + (value % 10)); // the modulo will be optimized by the JIT because 10 is a constant
        value /= 10; // same story.

        if (s[i] != expected) { return false; }
    }
    return true;
}

如果你没有负数,那么可以通过删除1来进行明显的优化。

你能做得更快吗?当然可以...这就是我首先发布C++链接的原因。大多数这些算法可以很容易地调整到这个“相等”的情况。

最后一个解决方案的可选优化

您可以使用10log来确定字符串的长度。这意味着有一个整数的下限和上限值。一个简单的查找表可以为您完成此操作。但是,如果未正确实现,10log会非常慢,因此请务必测试它!

哪一个更快

构建适当的测试并进行测试。我尝试在这里测试它,但没有您数据的特征,这可能会产生差异。

当然,如果您不需要如此直接的性能,请使用标准实现和equals,并进行测试。


当两个字符串相等时,您的方法较慢,但当它们不相等时,它会更快,这是预期的。 - assylias
@assylias谢谢您进行测试。愿意分享两个函数的结果吗?我在我的笔记本电脑上用C#进行了快速测试,发现IntToString需要1秒/StringToInt需要1.2秒(100M次比较) - 所以我很想知道Java的结果。 - atlaste
我已经更新了我的回答。你的方法可以在1.5秒内处理100M个相等的5位数字/字符串,但如果它们不相等,则只需要0.4秒。这是在我的台式电脑上完成的,该电脑相当强大。 - assylias
1
@assylias 好笑的是分支预测咬了我的屁股。谢谢你分享结果,现在一切都取决于他的数据了,我想 :-) - atlaste
@atlaste,如果value=9999,s="9";value = 0,s="+0"。我已经更新了我的问题,就像你的EqualsIntString()函数一样。 - lab bhattacharjee
正如我所说,有很多边界情况,比如s="apehaar"、千位分隔符等等。通常情况下,你可以通过在生成字符串时修复区域设置(或更精确地说是算法)来避免这些问题,否则无论你选择哪种解决方案,都会带来很多痛苦。例如:请注意,在荷兰和英国的区域设置中,“,”和“.”意味着完全不同的东西(千位分隔符/小数),没有什么魔法可以解决这个问题,即使使用Integer.parseInt也无法得到你想要的结果。 - atlaste

0

我敢打赌

Integer.parseInt(str) == integer

比较快。它只需要每个数字执行一次乘法和一次加法,而 Integer.toString 需要每个数字执行除法和模运算——最慢的 int 算术运算。

实际上,有一些优化可以减少 Integer.toString 中的操作数,但速度比仍然太大。

此外,还有为新字符串分配内存的问题。

另一方面,Integer.parseInt 接受所有 Unicode 数字,而不仅仅是 0 到 9,这会严重减慢其速度。我猜 Guava 的 Ints.parseInt 要快得多。


如果字符串无法解析,则会出现NumberFormatException。您可以捕获它,但这需要很多时间,而Integer.toString则是最佳选择。

-1
我会选择Interger.Parse()方法,这个方法在面对不同的本地化文化时更加弹性和适用性比其他方法要好...
速度并不是问题 - 有效的结果才是关键。

我喜欢匿名的踩票...没关系 - 慢慢算出错误的答案 - 我也不在乎哈哈 - Martin Milan
如果要求投下反对票的人发表声明说明原因,那么这个网站可以得到极大的改善。如果必要的话,他们可以保持匿名,但在很多情况下(不仅仅是这种情况),那些白痴(抱歉-尊贵的社区成员...)所思考的可能有价值。我们应该捕捉到这一点。 - Martin Milan
@MartinMilan 这个提议在 Meta 上已经被多次提出并被拒绝。其中一个原因是它几乎总是会导致冗长的讨论,但收获不多。 +++ 我没有给你点踩,但请注意 OP 询问了速度问题,而你没有提到它。 - maaartinus
@MartinMilan '[...] 在得到正确答案方面[...]'-- 抱歉,我不认为你真正理解了问题,所以让我直言不讳。我经常问关于性能的问题,总有人告诉我要避免'过早优化',并且我不应该这样做...就个人而言,我觉得这很烦人:我们在互相浪费时间...毕竟,如果我想知道'正确的方法',我本来就会问那个问题。因此,我真的不认为你的回答是'正确的答案',因为它没有回答问题,问题是“哪个更快”。 - atlaste
如果你只关心速度而不关心其他方面,那么我的回答可能并没有什么帮助...让我同样直言不讳 - 最快引发异常的人并不会得到额外的分数...无论如何,这个话题可以永远讨论下去,所以请放心,我对你怀有一颗温暖的心,并且你可以随意编写代码。 - Martin Milan
显示剩余2条评论

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