https://bird-liver-ecc.notion.site/StringTokenizer-VS-String-split-7133dade7c804f46a48994859fc514b0?pvs=4

 

StringTokenizer VS String.split()

StringTokenizer 와 String.split() 의 속도 차이와 그 이유

bird-liver-ecc.notion.site

↑ 원본링크 

 

 

- 참조 -

StringTokenizer 와 String.split() 의 속도 차이와 그 이유

StringTokenizer는 느릴까? 실험하기

StringTokenizer 관련 StackOverflow 게시글

 

 

무엇이 더 빠른가? 🏃‍♂️

  • 알고리즘 문제를 풀면서 각종 해설에서 StringTokenizer를 사용한다. 그래서 당연히 String.split() 은 속도를 생각하면 사용하면 안되는 메소드로 생각하고 있었다. 그런데 생각보다 그렇게 단순히 결정짓고 넘어갈 문제가 아니었다.
  • 결론 :
    1. 그때그때 다르다.
    2. StringTokenizer는 레거시코드다. (deprecated 되지는 않았다)
    3. String.split() 은 비교적 일정한 속도를 내지만 StringTokenizer는 경우에 따라 편차가 크다.
    4. 즉 상황에 맞춰서 써라. ( 자바 버전을 업그레이드 했을 때 문제 생길지는 미지수 jdk17까지는 사용할 수 있는듯 )

속도 차이가 왜 생기나? 🤷‍♂️

  • String.split()은 정규표현식을 사용해 문자열을 나누고 이는 속도가 느린 원인이 된다.
    • 실제로 알고리즘 문제를 풀다보면 속도차이가 꽤 생기는 경우 발생
    • 하지만 속도가 꽤나 균일하다
  • StringTokenizer는 구분자(delimeter)와 문자열을 전부 다 비교한다.
  • StringTokenizer는 ‘구분자가 유니코드, hasMoreTokens나 nextToken 호출’ 시에도 문자열과 구분자 전체를 비교하기 때문에 효율이 좋지 못하다.
    • 즉 위에서 말한 조건이 만족되는 경우가 많을수록 속도가 급속도로 느려진다.
    • 예를들어… 아스키코드에 존재하지 않는 “뷁” 같은 문자를 구분자로 사용하며 그 수가 많고, 여러 문자를 나누어 hasMoreTokens()나 nextToken() 을 반복해서 사용한다면 StringTokenizer를 사용하는건 좋은 선택이 아니다.

실험 결과 비교하기

  1. 첫번째 실험 : 구분자를 “,” 사용 / hasMoreTokens() 사용 안함 / nextToken() 한번 사용
    1. 결과 :
    splits ====== time : 271, clientIp : 192.168.1.1
    tokenizer ====== time : 80, clientIp : 192.168.1.1
    [출처] [StringTokenizer VS String.split] 누가 더 빠른가|작성자 평범한개발자
    
  2. 두번째 실험 : 구분자를 아스키코드가 아닌 문자 사용 / hasMoreTokens(), nextToken() 반복 사용
    1. 코드 :
    StringTokenizer 와 String.split() 의 속도 차이와 그 이유b. 결과 : 차이가 갑자기 확 좁혀졌다
  3. splits ====== time : 257, clientIp : 127.0.0.3 tokenizer ====== time : 220, clientIp : 127.0.0.3 [출처] [StringTokenizer VS String.split] 누가 더 빠른가|작성자 평범한개발자
  4. // 위 링크에서 코드 발췌 @Test public void getClientIpSplitTest() { String clientIp = null; long start = System.currentTimeMillis(); for(int i = 0; i < 1000000; i++) { String xForwardedFor="192.168.1.1뛟127.0.0.1뛟127.0.0.2뛟127.0.0.3"; String[] splits = xForwardedFor.split("뛟"); if(splits.length < 1) clientIp = ""; else { for(String str : splits) { clientIp = str; } } } long end = System.currentTimeMillis(); log.info("splits ====== time : {}, clientIp : {}", end-start, clientIp); } @Test public void getClientIpStringTokenizerTest() { String clientIp = null; long start = System.currentTimeMillis(); for(int i = 0; i < 1000000; i++) { String xForwardedFor="192.168.1.1뛟127.0.0.1뛟127.0.0.2뛟127.0.0.3"; StringTokenizer tokenizer = new StringTokenizer(xForwardedFor, "뛟"); while(tokenizer.hasMoreTokens()) { clientIp = tokenizer.nextToken(); } if(clientIp == null) clientIp = ""; } long end = System.currentTimeMillis(); log.info("tokenizer ====== time : {}, clientIp : {}", end-start, clientIp); } [출처] [StringTokenizer VS String.split] 누가 더 빠른가|작성자 평범한개발자
  5. 세번째 실험 : 두번째 실험에서 구분자 “하나” 추가 / hasMoreTokens(), nextToken() 반복 사용
    1. 결과 : 역전 당했다. 효율이 정말 좋지 않아보인다.
    "192.168.1.1뛟127.0.0.1뛟127.0.0.2뛟127.0.0.3뛟127.0.0.4";
    
    splits ====== time : 270, clientIp : 127.0.0.4
    tokenizer ====== time : 319, clientIp : 127.0.0.4 
    [출처] [StringTokenizer VS String.split] 누가 더 빠른가|작성자 평범한개발자
    

https://bird-liver-ecc.notion.site/BufferedReader-BufferedWriter-StringBuilder-0ef39a74daab465299f2c368513da602?pvs=4

 

BufferedReader, BufferedWriter, StringBuilder 의 사용법과 속도, 주의사항

BufferedReader

bird-liver-ecc.notion.site

BufferedReader

  • 버퍼를 사용하여 입력을 받는다.
  • Scanner는 띄어쓰기, 개행문자를 기준으로 값을 받는다.

장점

  1. Buffer를 사용해 한번에 묶어서 처리하므로 Scanner에 비해 속도가 빠름
  2. BufferedReader 버퍼크기 : 8192 char / Scanner 버퍼크기 : 1024 char
  3. 따라서 입력이 많은 경우 속도면에서 유리
  4. Scanner는 동기화 미지원, BufferedReader는 동기화 지원 → 멀티스레드 환경에서 안전하다.

단점

  1. BufferedReader는 문자열만 입력받기 때문에 이후 Integer.parseInt() 같은 후처리가 필요함. Scanner는 nextInt() 와 같이 원하는 자료형으로 쉽게 처리가능.
  2. IOException 예외 처리 필요 : try / catch 혹은 throw 사용
  3. 공백 단위로 데이터를 처리하려면 StringTokenizer 혹은 String.split() 사용

Scanner와의 속도 비교

  • 정수를 한 줄씩 읽고 합치기 1000만번 반복

입력 방식 수행시간(초)

java.util.Scanner 6.068
java.io.BufferedReader 0.934

사용법

BufferedReader br = new BufferedReader(new InputStreamReader(System.in);
String s = br.readLine();

// 형변환 예시
int i = Integer.parseInt(s);

// 공백 단위 데이터 처리 예시 1
// 인자1: 문자열, 인자2: 구분자(옵션)
StringTokenizer st = new StringTokenizer(br.readLine(), " ");
Stirng word = null;
while(st.hasMoreTokens()){
	word = st.nextToken();
}

// 공백 단위 데이터 처리 예시 2
String arr[] = br.readLine().split(" ");

BufferedWriter

  • 보통 사용하는 System.out.println() 대신 사용
  • 적은 양을 출력할 땐 System.out.println() 이 편리하지만 많은 양을 출력할 땐 이 방법은 속도가 느리다. 그래서 BufferedWriter 를 사용

개행 문자 출력

  • BufferedWriter는 개행을 자동으로 해주지 않는다.
  • BufferedWriter.newLine() 이나 BufferedWriter.write(”\n”) 사용

flush() / close()

  • 버퍼를 사용하기 때문에 더 이상 사용하지 않으면 flush()로 스트림을 비우고, close() 로 출력스트림을 닫아줘야한다.
  • 한번 출력 후 다른 것도 출력하려면 flush() 로 비우기

사용법

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String str = "hello";

// void write(char[] buf, int offset, int length)
// 버퍼 offset부터 length 크기만큼 write
bw.write(str + "\\n"); // 혹은 bw.newLine()
bw.flush();
bw.close();

StringBuilder (String, StringBuffer 와 비교)

참고한 링크 BufferedReader, BufferedWriter, StringBuilder 설명 Java 문자열 concat, +, StringBuilder 차이 Java의 String, StringBuffer, StringBuilder 차이, 실험 오라클 자바 문서 : String 클래스

  • String 클래스는 불변 속성, StringBuffer/StringBuilder 는 가변속성을 가짐
    • String 값을 문자열 연산으로 변경 → 원래 값을 버리고 새로운 값을 할당 ( 원래 값은 Heap 메모리에 남아있다가 GC에 의해 삭제 )
    • 주의 : 위 이유로 문자열 연산이 많이 일어나는 경우 String을 사용하면 Heap 메모리에 많은 Garbage가 쌓인다. 이는 메모리 낭비를 유발함
  • 따라서 문자열 연산이 많은 경우 StringBuffer / StringBuilder 사용
  • StringBuffer VS StringBuilder
    • 두 클래스가 제공하는 메소드는 같음
    • StringBuffer : 동기화를 지원 → 멀티스레드 환경에서 안전
    • StringBuilder : 동기화 지원 x → 멀티스레드 환경에 적합하지 않음. 단, 단일스레드에서는 StringBuffer 보다 빠름
    • 예를 들어, 멀티스레드 환경에서 문자열을 수정할 때 StringBuffer는 각 스레드에서 같은 문자열을 확인할 수 있지만, StringBuilder의 경우 동기화를 지원하지 않기 때문에 문제가 발생할 수 있다.
  • StringBuilder 주요 메소드
    • append(String s)
      • StringBuilder 뒤에 값을 붙임
    • delete(int start , int end)
      • 특정 인덱스부터 인덱스까지를 삭제
    • insert(int offet, any primitive of a char[])
      • 문자를 삽입함
    • replace(int start , int end , String s)
      • 일부를 String 객체로 치환
    • reverse()
      • 순서를 뒤집음
    • setCharAt(int index , char ch)
      • 주어진 문자를 치환
    • indexOf(String s)
      • 값이 어느 인덱스에 들어있는지 확인
    • subString(int start, int end)
      • start와 end 사이의 값을 잘라옴.

String “+” / StringBuilder.append() / StringBuffer.append() 비교 실험🧪🧪

  • 약 5000 밀리초 : 1밀리초 : 1밀리초
    • StringBuilder, StringBuffer가 String 연산에 비해 엄청난 속도를 보여준다.  (맨 아래 수정사항 참조)
  • StringBuilder 와 StringBuffer의 차이는 거의 없는 듯하다
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class Main {
    public static void main(String[] args) throws IOException {

       BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        String hello = "hello";
        String hellohello = "";
        StringBuilder sBuilder = new StringBuilder();
        StringBuffer sBuffer = new StringBuffer();
				long start = 0L;
        long end = 0L;

        // 1. String + 
        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            hellohello = hellohello + hello;
        }
        end = System.currentTimeMillis();
        bw.write("String + : " + String.valueOf(end - start) + "\\n");

        // 2. StringBuilder.append()
        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            sBuilder.append("hello");
        }
        end = System.currentTimeMillis();
			//bw.write(sBuilder.toString() + "\\n");
        bw.write("StringBuilder : " + String.valueOf(end - start) + "\\n");

        // 3. StringBuffer.append()
        start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            sBuffer.append("hello");
        }
        end = System.currentTimeMillis();
			//bw.write(sBuffer.toString() + "\\n");
        bw.write("StringBuffer : " + String.valueOf(end - start) + "\\n");

        bw.flush(); bw.close();
    }
}

실험에 대한 의문….🤔🤔

  • 위 실험이 잘못된건가 싶어서 구글링해보았으나 다른 사람들의 결과도 비슷했다. 똑같이 10만번 반복한 사람의 결과는 내 결과인 약 4~5초와 비슷했다. ( String 의 + 연산 )
    • 이 분은 StringBuilder.append() 연산에서 결과값을 계속 재할당하셨는데 이유는 잘 모르겠다. StringBuilder 실험결과는 꽤 차이가 났는데 재할당이 속도 저하의 원인으로 보인다.
    • str = str.append("world") 로 작성하셨고 약 200밀리초 걸림. 내 실험결과는 1~2 밀리초. str 변수는 StringBuilder의 인스턴스였고 내가 실험했을 때 결과도 추가된 문자열이 잘 출력되어 위 코드는 불필요한 대입인 것으로 보인다.
  • String의 + 연산은 jdk 1.5 미만에서는 매번 String 인스턴스를 생성하여 더하는 방식을 취했고, 그 이후에는 StringBuilder로 컴파일 되도록 변경되었다. 때문에 +를 활용해도 큰 차이가 없다… 는 이야기를 보았다.
  • 하지만, 직접 실험해본 결과 속도에서 너무나도 큰 차이가 난다. 물론 10만번으로 많은 반복을 거쳤지만 그래도 무시할만한 차이는 아니다.
  • oracle 의 java 문서에서 확인한 결과
    • 실제로 문자열 합치는 기능이 append 메소드를 사용한다고 한다. 그런데 왜 이렇게 많은 차이가 발생하는걸까?
  • The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method. String conversions are implemented through the method toString, defined by Object and inherited by all classes in Java. For additional information on string concatenation and conversion, see Gosling, Joy, and Steele, The Java Language Specification.
  • 관련 내용을 찾을 수 없어 chatGPT 에게 질문한 결과는 다음과 같다.

------------------------------------------------------------------------------------------

네, **String**의 + 연산이 내부적으로 **StringBuilder**를 항상 사용하는 것은 아닙니다.

자바 컴파일러는 + 연산자로 문자열을 연결하는 코드를 컴파일할 때, 효율적인 문자열 연결을 위해 내부적으로 **StringBuilder**를 사용하는 방식으로 변환합니다. 이를 "컴파일러가 자동으로 **StringBuilder**를 사용한다"고 생각할 수 있습니다.

따라서 컴파일러는 다음과 같은 코드:

String str = "Hello";
str = str + " World";

다음과 같이 컴파일됩니다:

String str = "Hello";
str = new StringBuilder().append(str).append(" World").toString();

이렇게 **StringBuilder**를 사용하여 문자열 연결이 수행됩니다. 이러한 변환은 컴파일 과정에서 발생하며, 개발자가 직접 **StringBuilder**를 사용하는 것과는 다른 방식입니다.

하지만, + 연산자로 문자열을 반복적으로 연결하는 경우에는 컴파일러가 최적화를 수행하지 못하고 내부적으로 **StringBuilder**를 사용하지 않습니다. 반복적인 + 연산은 매 연산마다 새로운 **StringBuilder**를 생성하고 문자열을 누적하는 비용이 발생하므로, 성능에 영향을 줄 수 있습니다. 이 경우에는 명시적으로 **StringBuilder**를 사용하는 것이 더 효율적입니다.

따라서, + 연산자를 사용하여 문자열을 연결할 때는 개발자가 컴파일러의 동작을 고려할 필요가 없으며, 코드를 가독성 좋게 작성하는 것에 집중하는 것이 좋습니다. 컴파일러는 최적화를 수행하여 효율적인 문자열 연결을 처리할 수 있습니다.

 

------------------------------------------------------------------------------------------

 

  • gpt 답변을 완전히 신뢰할 수 있는지는 모르겠지만 저 말이 맞다면 StringBuilder를 사용하긴 하지만 매 연산마다 새로운 인스턴스를 생성하는 건 여전하다. 이 부분에서 오버헤드가 발생하는지도 모르겠다.
  • 추측 : 아무래도 String 클래스가 불변 속성이다보니 StringBuilder를 사용하더라도 매번 새로운 인스턴스를 만들어 적용할 수 밖에 없는 듯하다. 위 gpt 답변에서 보이듯이 StringBuilder.append().toString()으로 결국 String 클래스로 변환해서 값을 대입해주기 때문이다. 따라서 기존 문자열 대신 새로운 문자열을 넣어주기 위해 매번 새로운 인스턴스를 생성하는 수 밖에 없어보인다.
  • 실험 후 결론 : 속도가 중요한 상황에서 반복적인 문자열 concatenation 이 필요하다면, String의 “+” 보다는 StringBuilder.append() 를 사용하자. 그리고 반복이 끝난 후 toString() 으로 한번에 변환해서 사용하자.
  • 23.09.20 : 시간이 지나서 전에 썼던 글을 다시 보았더니 수정할게 있다. 추측부분에서 새로운 문자열을 할당하기 위해 매번 새로운 인스턴스를 생성한다고 했지만 틀렸다. 내가 for 문에서 hellohello = hellohello + hello; 를 계속 반복하는 바람에 변수에 새로운 String 인스턴스를 10만번 반복해서 할당하는 꼴이되었다. 그러니 오버헤드가 클 수 밖에 없었다. 만약 hellohello = hellohello + hello + bonjour + nihao + annyeong; 처럼 반복문 없이 단순히 + 연산자를 여러번 사용하는 경우라면 내부에서 StringBuilder로 한번에 처리하기때문에 변수에 새 인스턴스를 할당하는 동작은 한 번만 일어난다. 이럴 땐 StringBuilder나 별 차이가 없다. (물론 chatgpt가 한 답변이 옳은 경우라면… 말이다) 하지만 위에서 실험한 것 처럼 for문 같은 반복문안에서 + 연산자 및 인스턴스 할당을 사용할 거라면 StringBuilder를 직접 사용하는게 훨씬 나아보인다.

'학습 기록 > 알고리즘' 카테고리의 다른 글

StringTokenizer VS String.split()  (0) 2023.09.20

+ Recent posts