【问题标题】:Best way to convert a SUBSTRING to integer in java在java中将SUBSTRING转换为整数的最佳方法
【发布时间】:2023-03-18 02:41:01
【问题描述】:

在 java 中,不使用 Integer.parseInt 将子字符串转换为整数的最快方法是什么?我想知道是否有办法避免使用 parseInt,因为它需要我创建一个临时字符串,该字符串是我要转换的子字符串的副本。

"abcd12345abcd"

我想通过不使用子字符串来避免创建新的临时字符串。

如果我自己动手,有没有办法避免我在String.charAt(int) 中看到的数组边界检查的开销?

编辑

我从每个人那里得到了很多很好的信息......以及关于预优化的常见警告:) 基本答案是没有什么比 String.charAt 或 char[] 更好的了。不安全的代码即将被淘汰(也许)。编译器很可能可以优化掉对 [] 的过度范围检查。

我做了一些基准测试,由于不使用子字符串和滚动特定的 parseInt 而节省的费用是巨大的。

调用 Integer.parseInt(str.substring(4,8)) 的成本的 32% 来自子字符串。这不包括后续的垃圾收集成本。

Integer.parseInt 旨在处理非常广泛的输入。通过使用 charAt 滚动我自己的 parseInt(特定于我们的数据的样子),我能够在 substring 方法上实现 6 倍的加速

尝试 char[] 的评论导致性能提升约 7 倍。但是,您的数据必须已经在 char[] 中,因为转换为 char 数组的成本很高。对于解析文本,完全留在 char[] 中并编写一些函数来比较字符串似乎是有意义的。

基准测试结果(越小越快):

parseInt(substring)  23731665
parseInt(string)     16859226
Atoi1                 7116633
Atoi2                 4514031
Atoi3 char[]          4135355
Atoi4 char[]          3503638
Atoi5 char[]          5485495
GetNumber1            8666020
GetNumber2            5951939

在基准测试期间,我还试验了 Inline 的开启和关闭,并验证编译器正确地内联了所有内容。

如果有人关心,这是我的基准测试代码...

package javaatoi;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;

public class JavaAtoi {

    static int cPasses = 10;
    static int cTests = 9;
    static int cIter = 0x100000;
    static int cString = 0x100;
    static int fStringMask = cString - 1;

    public static void main(String[] args) throws InterruptedException {

        // setup test data.  Use a large enough set that the compiler 
        // wont unroll the loop.  Use a small enough set that we are 
        // keeping the data in L2.  I don't want to measure memory loads.

        String[] a = new String[cString];
        for (int i = 0 ; i< cString ; i+=4) {
            // leading zeros will occur, so add one number with one.
            a[i+0] = "abcd01234abcd";
            a[i+1] = "abcd1234abcd";
            a[i+2] = "abcd1234abcd";
            a[i+3] = "abcd1234abcd";
        }

        // array of pre-substringed stuff
        String[] a1 = new String[cString];
        for (int i=0 ; i< cString ; ++i)
            a1[i]= a[i].substring(4,8);

        // char array version of the strings
        char[][] b = new char[cString][];
        for (int i =0 ; i<cString ; ++i)
            b[i] = a[i].toCharArray();

        // array to hold times for each test for each pass
        long[][] t = new long[cPasses][cTests];

        // multiple dry runs to let the compiler optimize the functions
        for (int i=0 ; i<50 ; ++i) {
          t[0][0] = TestParseInt1(a)[0];
          t[0][1] = TestParseInt2(a1)[0];
          t[0][2] = TestAtoi1(a)[0];
          t[0][3] = TestAtoi2(a)[0];
          t[0][4] = TestAtoi3(b)[0];
          t[0][5] = TestAtoi4(b)[0];
          t[0][6] = TestAtoi5(b)[0];
          t[0][7] = TestAtoi6(a)[0];
          t[0][8] = TestAtoi7(a)[0];
        }

        // now do a bunch of tests
        for (int i=0 ; i<cPasses ; ++i) {
            t[i][0] = TestParseInt1(a)[0];
            t[i][1] = TestParseInt2(a1)[0];
            t[i][2] = TestAtoi1(a)[0];
            t[i][3] = TestAtoi2(a)[0];
            t[i][4] = TestAtoi3(b)[0];
            t[i][5] = TestAtoi4(b)[0];
            t[i][6] = TestAtoi5(b)[0];
            t[i][7] = TestAtoi6(a)[0];
            t[i][8] = TestAtoi7(a)[0];
        }

        // setup mins - we only care about min time.
        t[cPasses-1] = new long[cTests];
        for (int i=0 ; i<cTests ; ++i)
            t[cPasses-1][i] = 999999999;
        for (int j=0 ; j<cTests ; ++j) {
            for (int i=0 ; i<cPasses-1 ; ++i) {
                long n = t[i][j];
                if (n < t[cPasses-1][j])
                    t[cPasses-1][j] = n;
            }
        }

        // output string
        String s = new String();
        for (int j=0 ; j<cTests ; ++j) {
            for (int i=0 ; i<cPasses ; ++i) {
                long n = t[i][j];
                s += String.format("%9d", n);
            }
            s += "\n";
        }
        System.out.println(s);

        // if you comment out the part of TestParseInt1 you can sorta see the 
        // gc cost.
        System.gc(); // Trying to get an idea of the total substring cost
        Thread.sleep(1000);  // i dunno if this matters.  Seems like the gc takes a little while.  Not real exact...

        long collectionTime = 0;
        for (GarbageCollectorMXBean garbageCollectorMXBean : ManagementFactory.getGarbageCollectorMXBeans()) {
            long n = garbageCollectorMXBean.getCollectionTime();
            if (n > 0) 
                collectionTime += n;
        }

        System.out.println(collectionTime*1000000);
    }

   // you have to put each test function in its own wrapper to 
   // get the compiler to fairly optimize each test.
   // I also made sure I incremented n and used a large # of string
   // to make it harder for the compiler to eliminate the loops.

    static long[] TestParseInt1(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        // comment this out to get an idea of gc cost without the substrings
        // then uncomment to get idea of gc cost with substrings
        for (int i=0 ; i<cIter ; ++i) 
            n += Integer.parseInt(a[i&fStringMask].substring(4,8));
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestParseInt2(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Integer.parseInt(a[i&fStringMask]);
        return new long[] { System.nanoTime() - startTime, n };
    }


    static long[] TestAtoi1(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi1(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi2(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi2(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi3(char[][] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi3(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi4(char[][] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi4(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi5(char[][] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi5(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi6(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi6(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static long[] TestAtoi7(String[] a) {
        long n = 0;
        long startTime = System.nanoTime();
        for (int i=0 ; i<cIter ; ++i) 
            n += Atoi7(a[i&fStringMask], 4, 4);
        return new long[] { System.nanoTime() - startTime, n };
    }

    static int Atoi1(String s, int i0, int cb) {
        int n = 0;
        boolean fNeg = false;   // for unsigned T, this assignment is removed by the optimizer
        int i = i0;
        int i1 = i + cb;
        int ch;
        // skip leading crap, scan for -
        for ( ; i<i1 && ((ch = s.charAt(i)) > '9' || ch <= '0') ; ++i) {
            if (ch == '-') 
                fNeg = !fNeg;
        }
        // here is the loop to process the valid number chars.
        for ( ; i<i1 ; ++i) 
            n = n*10 + (s.charAt(i) - '0'); 
        return (fNeg) ? -n : n;
    }

    static int Atoi2(String s, int i0, int cb) {
        int n = 0;
        for (int i=i0 ; i<i0+cb ; ++i) {
            char ch = s.charAt(i);
            n = n*10 + ((ch <= '0') ? 0 : ch - '0');
        }
        return n;
    }

    static int Atoi3(char[] s, int i0, int cb) {
        int n = 0, i = i0, i1 = i + cb;
        // skip leading spaces or zeros
        for ( ; i<i1 && s[i] <= '0' ; ++i) { }
        // loop to process the valid number chars.
        for ( ; i<i1 ; ++i) 
            n = n*10 + (s[i] - '0');
        return n;
    }   

    static int Atoi4(char[] s, int i0, int cb) {
        int n = 0;
        // loop to process the valid number chars.
        for (int i=i0 ; i<i0+cb ; ++i) {
            char ch = s[i];
            n = n*10 + ((ch <= '0') ? 0 : ch - '0');
        }
        return n;
    }   

    static int Atoi5(char[] s, int i0, int cb) {
        int ch, n = 0, i = i0, i1 = i + cb;
        // skip leading crap or zeros
        for ( ; i<i1 && ((ch = s[i]) <= '0' || ch > '9') ; ++i) { }
        // loop to process the valid number chars.
        for ( ; i<i1 && (ch = s[i] - '0') >= 0 && ch <= 9 ; ++i) 
            n = n*10 + ch;
        return n;
    }   

    static int Atoi6(String data, int start, int length) {
        int number = 0;
        for (int i = start; i <= start + length; i++) {
            if (Character.isDigit(data.charAt(i))) {
                number = (number * 10) + (data.charAt(i) - 48);
            }
        }       
        return number;
    }

    static int Atoi7(String data, int start, int length) {
        int number = 0;
        for (int i = start; i <= start + length; i++) {
            char ch = data.charAt(i);
            if (ch >= '0' && ch <= '9') {
                number = (number * 10) + (ch - 48);
            }
        }       
        return number;
    }

}

【问题讨论】:

  • 那么如果字符串是“ABC123DEF456”,那么结果整数是123还是123456?
  • @BrandonLing:不完全;你必须先去掉非数字字符。
  • 数字部分是否总是从索引4开始?
  • 子字符串总是数字。我们在编译时也知道字符串中的位置和长度。
  • 字符串是不可变的;对 String 进行的任何操作都会创建一个新操作。除非您真的想处理数组,否则创建新字符串的开销是如此微不足道,在我们知道这是一个大问题之前,没有理由尝试围绕它编写代码。

标签: java string integer type-conversion


【解决方案1】:

对不起...如果没有任何一个,真的没有办法完成你想做的事情:

  • 创建中间String,或
  • 创建一些其他中间对象来代替 String,然后将其解析为 int

Java 不像 C++; a String isn't the same as a char[].

正如我之前提到的,在 String 上执行的任何返回 String 的操作都会产生一个 String 实例,因此不可避免地,您将在一个 Strings 中处理中级时尚。

这里的主要问题是,如果你真的知道子字符串的界限,那么使用它们来完成你需要做的事情。

Do not worry about optimization 直到您可以推断出这部分代码是最大的瓶颈。即使这样,也要坚持有意义的优化;您可以将整个 String 转换为 IntStream,并且只解析 Java 8 中的实际数字元素。

很有可能这段代码不会对性能造成重大影响,过早地优化它会导致你走上一条非常、非常痛苦的道路。

实际上,您可以获得的最接近(使用 Java 8 的 Stream API)是在 CharacterString 之间进行一些转换,但这仍然会创建中间 Strings:

System.out.println(Integer.parseInt("abcd12345abcd".chars()
                                                   .filter(Character::isDigit)
                                                   .mapToObj(c -> (char) c)
                                                   .map(Object::toString)
                                                   .reduce("", String::concat)));

...这比这更难读和理解:

System.out.println(Integer.parseInt("abcd12345abcd".substring(4, 9)));

【讨论】:

  • 不,你在这方面相当错了。如果字符串中存在不在其基值范围内的字符,parseInt爆炸
  • 嗯,我想的每件事都有第一次
  • 在您的两个解决方案中,您是否仍在使用 OP 试图避免的临时字符串?
  • @Shar1er80:我发誓……是的。 这就是重点。尝试以任何其他方式编写它是迂腐、低效和低效的。 Java 不像 C++。在您真正知道这是瓶颈之前,没有任何动力去尝试和优化它。
  • @Shar1er80:我基本上声明这是一个傻瓜的差事,所以它有点不同。在这方面学究气真的没有意义;有时答案是“不”。
【解决方案2】:

更新

看到您想在 Java 中模仿 C/C++ 行为,在进行了一些谷歌搜索后,我遇到了http://ssw.jku.at/Research/Papers/Wuerthinger07/ 你可能会感兴趣。

Java HotSpot™ 客户端编译器的数组边界检查消除 摘要

每当访问一个数组元素时,Java 虚拟机都会执行一个 比较指令以确保索引值在有效范围内 界限。这会降低 Java 程序的执行速度。大批 边界检查消除识别这种检查的情况 是多余的,可以删除。我们提出了一个数组边界检查 基于静态的 Java HotSpot™ VM 消除算法 在即时编译器中进行分析。

该算法适用于静态单一的中间表示 赋值形式并维护索引表达式的条件。它 如果可以证明它们永远不会失败,则完全删除边界检查。 只要有可能,它就会将边界检查移出循环。静态的 检查的数量保持不变,但循环内的检查可能 更频繁地执行。如果这样的检查失败,执行 程序回退到解释模式,避免了一个问题 在错误的地方抛出异常。

评估显示加速比接近理论最大值 科学 SciMark 基准套件(平均 40%)。算法 还提高了 SPECjvm98 基准测试套件的执行速度 (平均 2%,最大 12%)。

在这里找到完整的研究论文http://www.ssw.uni-linz.ac.at/Research/Papers/Wuerthinger07/Wuerthinger07.pdf

旧答案 2

由于您知道字符串中数字的开头和长度,因此您仍然可以“自己滚动”而无需进行边界检查。无论哪种方式,您都必须进行某种提取才能获得该号码。无论您是提取到临时字符串然后转换它,还是即时转换字符。

public static void main(String[] args) throws Exception {
    String data = "abcd12345abcd";
    System.out.println(getNumber(data, 4, 5));
}

public static int getNumber(String data, int start, int length)
{
    int number = 0;
    for (int i = start; i <= start + length; i++) {
        char c = data.charAt(i);
        if ('0' <= c && c <= '9') {
            number = (number * 10) + (c - 48);
        }
    }
    return number;
}

结果:

12345

旧答案 1

使用String.replaceAll() 删除您不需要的内容,然后转换/解析剩下的内容。

public static void main(String[] args) throws Exception {
    String data = "abcd12345abcd";

    int myInt = Integer.valueOf(data.replaceAll("[^0-9]", ""));
    System.out.println(myInt);
}

结果:

12345

【讨论】:

  • 他不想使用String.substringInteger.parseInt - 所以我无法想象他所追求的是正则表达式。
  • 这是一种方法。我正在寻找一种比制作临时子字符串并将其传递给 parseInt 更快的方法。这会生成一个临时字符串,并为启动做更多工作。不过谢谢!
  • 如果您要投反对票,请至少评论一下您投反对票的原因!!!更新的答案没有使用 substring() 或 parseInt()。
  • 啊,对不起。我投了反对票,因为 OP 建议对数组索引进行边界检查太慢,所以正则表达式会慢几个数量级。 charAt 对于他的需求来说也太慢了,你更新的答案表明了这一点。
  • @SeanBright 查看更新的答案。不是遍历整个字符串,而是提取数字。作为 OP cmets,“子字符串将始终是数字。我们在编译时也知道字符串中的位置和长度”
【解决方案3】:

请记住,这不是我通常处理这个问题的方式(选择使用正则表达式来过滤掉非数字)。但是,以下解决方案不会创建单独的字符串(除了字符数组)。


public static int getIntegerFromString(String s) {
    int multiplier, result = 0;
    boolean inIntegers = false, beforeInteger = true;
    char[] chars = s.toCharArray();
    char c;

    // Iterate through each character, starting at the end
    for(int i = chars.length - 1; i >= 0; i--) {
        c = chars[i];
        if(Character.isDigit(c)) {

            // The char is a digit, so we either increase the multiplier (if the previous char was also a digit) or prepare our environment
            if(inIntegers) {
                multiplier *= 10;
            }
            else {
                inIntegers = true;
                beforeInteger = false;
                multiplier = 1;
            }

            result += multiplier * Character.getNumericValue(c);
        }
        else if(inIntegers) {
            // We're done with the sequence of integers. Stop the for-loop.
            break;
        }
    }

    return result;
}

[chris@localhost:Projects]$ java Test 3949
3949
[chris@localhost:Projects]$ java Test 3949G
3949
[chris@localhost:Projects]$ java Test E3949G
3949

【讨论】:

  • toCharArray() 执行数组复制,我认为这比仅使用 charAt() 慢。
  • 是的。基本问题是是否有 parseInt 的类似物可以在原位对字符串中的一系列字符进行操作。从技术上讲,这不会创建临时字符串,但它确实调用了 toCharArray,这是同一件事。不过,我非常感谢您的尝试!
【解决方案4】:

您可以尝试查看 sun.misc.Unsafe。我实际上从未使用过它,但如果您想避免边界检查等,可以使用这个(未记录的)类来做到这一点。

https://stackoverflow.com/questions/5574241/how-can-sun-misc-unsafe-be-used-in-the-real-world

编辑: 关于 Java 9 中 Unsafe 的删除(作者认为由于许多库都使用它,因此删除它不是一个好主意):http://blog.dripstat.com/removal-of-sun-misc-unsafe-a-disaster-in-the-making/

也可以使用 JNI,但我猜将它调用为琐碎的方法会导致大量开销(如果边界检查已经定义为开销)

What makes JNI calls slow?

下面的链接可能也很有趣,作者还说经常调用但运行时间短的方法很难优化: https://thinkingandcomputing.com/2014/03/30/eliminating-jni-overhead/

您可以通过以下方式获取 Unsafe:

    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);

详情见:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

不安全数组示例:

    int[] x = new int[]{1,2,3,4};
    final int offset = unsafe.arrayBaseOffset(int[].class);
    final int arrayIndexScale = unsafe.arrayIndexScale(int[].class);
    for (int i=0;i<4;i++){
        unsafe.putInt(x, offset+arrayIndexScale*i, 11*(i+1));
    }
    System.out.println(Arrays.toString(x));
  Output: [11, 22, 33, 44]

【讨论】:

  • 你真的在这里挥手致意。为什么你会认为Unsafe 是一个好用的东西?
  • 更不用说 Unsafe 在 Java 9 中被杀死了,所以这不是向前兼容的。
  • 好吧 OP 的问题假设创建额外的 String 对象或边界检查是开销,所以这可能是避免这种情况的一种可能性。再说一次,也可以使用 C。我并不是说这是一个好主意,因为这显然违反了 Java 的设计原则、过早优化等,但作为“C++ 人”,他希望知道自己在做什么。
  • 当我提出绩效问题时,我会遇到大量动机和理智问题。
  • +1 因为这个答案很有帮助。我学到了一些东西。但是,如果在将来的 java 中将其删除,那么我将无法使用它。使用 native 并编写一个 c 函数来做呢?
猜你喜欢
  • 2023-04-08
  • 2011-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-18
  • 2020-09-05
  • 1970-01-01
  • 2010-09-12
相关资源
最近更新 更多