Language/Java

[Java] StringBuilder vs System.out.println() 성능 비교

사과만쥬 2024. 8. 6. 20:14

메인 언어로 파이썬을 쓰지만코테는 파이썬이 최고, 자바만 됐던 코테또토에버를 경험하고 나서는 자바로도 코테 연습을 조금씩 하고 있습니다.

사실 StringBuilder를 생활화 하라는 이야기를 듣기만 했지, 아직까지는 sout이 편해서 System.out.println()을 도배하면서 썼습니다. 그러나, 최근 풀었던 문제에서 sout로 도배했더니 시간초과가 났습니다. 코드는 아래에 첨부하겠습니다.

 

 

 

풀이한 문제

https://www.acmicpc.net/problem/15651

맨 아랫줄 에러는 st.nextToken() 자리에 br.readLine()을 써서 NumberFormat 에러가 났습니다..

 

시간초과 난 코드

import java.util.*;
import java.io.*;

public class Main {
    static int result[];

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        result = new int[m];
        dfs(0, n, m);
    }

    public static void dfs(int level, int n, int m) {
        StringBuilder sb = new StringBuilder();
        if (level==m) {
            for (int i = 0; i<m; i++) {
                sb.append(result[i]+" ");
            }
            System.out.println(sb);
            return;
        }
        for (int i = 0; i<n; i++) {
            result[level] = i+1;
            dfs(level+1, n, m);
            result[level] = 0;
        }
    }
}

(이 코드는 그나마 sb를 조금이라도 쓰려고 했지, 이전 코드는 sout만 도배되어 있었습니다.)

 

 

시간초과 나지 않은 정답코드

import java.util.*;
import java.io.*;

public class Main {
    static int result[];
    static StringBuilder sb = new StringBuilder();

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());
        result = new int[m];
        dfs(0, n, m);
        System.out.println(sb.toString());
    }

    public static void dfs(int level, int n, int m) {

        if (level==m) {
            for (int i = 0; i<m; i++) {
                sb.append(result[i]+" ");
            }
            sb.append("\n");
            return;
        }
        for (int i = 0; i<n; i++) {
            result[level] = i+1;
            dfs(level+1, n, m);
            result[level] = 0;
        }
    }
}

 

그래서 StringBuilder와 System.out.println()의 코드를 까봤습니다.

 


 

StringBuilder

모바일은 아래를 참고해주세요.

 

StringBuilder에 대한 설명입니다. 

mutable sequence of characters.  This class provides an API compatible with {@code StringBuffer}, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement for {@code StringBuffer} in places where the string buffer was being used by a single thread (as is generally the case).  Where possible, it is recommended that this class be used in preference to {@code StringBuffer} as it will be faster under most implementations.

 

위와 같습니다. 주요 특징들에 대해서 언급하면,

 

- mutable

- used by a single thread

- it is recommended that this class be used in preference to {@code StringBuffer} as it will be faster under most implementations.

 

크게 세 가지 특징을 들 수 있습니다.

 

1) mutable한 객체

자바를 배웠다면 아시겠지만, 자바의 String 객체는 불변 객체(immutable)입니다.

StringBuilder를 사용하지 않으면 문자열을 합치는 작업을 수행할 때마다 새로운 String 객체가 생성됩니다. 예를 들어, a + b + c와 같이 문자열을 더할 때마다 새로운 String 객체가 만들어지고, 기존의 문자열 데이터를 복사해야 합니다. 이 과정은 메모리 사용량이 많고, 가비지 컬렉션을 유발할 수 있습니다.

그러나 StringBuilder는 가변 객체로, 내부의 문자열 버퍼를 사용하여 문자열을 수정할 수 있습니다. 새로운 객체를 생성하지 않고도 문자열을 추가하거나 변경할 수 있어 메모리와 연산 시간을 절약할 수 있습니다. 또한 StringBuilder는 초기 버퍼 크기를 설정할 수 있으며, 버퍼가 꽉 차면 크기를 동적으로 증가시킵니다. 이는 문자열을 빈번하게 합치는 경우 효율적입니다.

 

2) 일반적인 경우 싱글 스레드에서 사용됨

 

3) 다른 구현체들에 비해 빠르기 때문에 사용을 추천함

(위에서 언급하고 있는 StringBuffer의 경우에는 멀티 스레드에서 사용하기 때문에 느릴 수밖에 없음)

 

사실, 코드를 까보지 않고도 '빠르기 때문에 추천함'이라는 문구를 보면 안 쓸 이유가 없긴 합니다. 그렇지만 이왕 시작한 거, 제가 이해하는 부분까지 작성해 보려고 합니다.

 

문서를 좀 더 읽어보도록 하겠습니다.

 

The principal operations on a StringBuilder are the append and insert methods, which are overloaded so as to accept data of any type. Each effectively converts a given datum to a string and then appends or inserts the characters of that string to the string builder. The append method always adds these characters at the end of the builder; the insert method adds the characters at a specified point.

 

=> 어떤 타입이든 받을 수 있게 하기 위해 append와 insert 메서드를 넣음

=> 주어진 데이터를 문자열로 효과적으로 변환한 다음 해당 문자열의 문자를 문자열 작성기에 추가하거나 삽입

=> append는 항상 빌더의 끝에 문자들을 삽입, insert는 정해진 위치에 삽입

 

여기서는 제가 보려고 했던 성능적인 이야기는 없고 일반적인, 여러 언어를 배우면서 흔히 나오는 이야기니 생략하겠습니다.

 

In general, if sb refers to an instance of a StringBuilder, then sb.append(x) has the same effect as sb.insert(sb.length(), x).

 

일반적으로, append나 insert나 효과는 같다고 합니다.

 

StringBuilder implements Comparable but does not override equals.

 

equals를 override하지는 않는다고 합니다.

 

 

 

코드를 깊게 파보지 않고도 건질 수 있는 내용은, 일단 빠르니까 써라! 인듯 합니다.

 

 


 

System.out.println()

 

sout의 경우에는, 다양한 의견을 내줬습니다.

 

 

제가 아는, 여러 언어로 알고리즘 하시는 분의 카톡을 보고 좀 더 찾아봤습니다.

 

쓰면서 코드를 까보려고 했는데, 생각보다 운영체제를 더 깊게 파고드는 제 모습을 발견했습니다(...)

 

 

 

StringBuilder는 반복적으로 문자열을 합치는 경우 기존의 버퍼에 문자열을 추가할 수 있으므로 성능이 크게 향상됩니다.

이 과정에서 I/O는 발생하지 않습니다.

 

그러나 매번 println을 해서 문자열을 합치면, 각 호출마다 String 객체가 생성되고 이를 출력하는 과정이 반복되므로 성능이 저하됩니다. 이 과정에서 I/O 작업이 빈번해져서 오버헤드가 발생하여 성능이 저하됩니다.

 

 

 

 

 

 


실제 실행시간 테스트

public class Test {
    public static void main(String[] args) {
        int n = 100; // 테스트할 문자열 길이

        // StringBuilder를 이용하여 한 번에 출력하는 방법
        long startTime = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append('a' + "\n");
        }
        System.out.println(sb.toString());
        long endTime = System.nanoTime();
        long durationWithBuilder = endTime - startTime;
        System.out.println("StringBuilder 사용 시간: " + durationWithBuilder + " 나노초");

        // 한 글자씩 println을 호출하는 방법
        startTime = System.nanoTime();
        for (int i = 0; i < n; i++) {
            System.out.println('a');
        }
        endTime = System.nanoTime();
        long durationWithPrintln = endTime - startTime;
        System.out.println("한 글자씩 println 사용 시간: " + durationWithPrintln + " 나노초");
    }
}

 

위와 같이 코드를 짜서 println을 이용했을 때하고 StringBuilder를 이용했을 때 시간 차이가 얼마나 나는지 알아봤습니다.

 

인코딩 깨지는건 무시해주세요..

StringBuilder의 경우 205500 나노초가 걸렸고, println의 경우 1747700 나노초가 걸렸습니다.

 

대략 8.5배 정도 차이나는 것을 알 수 있습니다.

(테스트 환경에 따라 걸리는 시간 차이는 달라질 수 있습니다.)

데이터 갯수가 더 많아지면, 더 많은 차이가 나겠죠?

 

 


참고하면 좋은 자료

https://www.youtube.com/watch?v=gc7bo5_bxdA

'Language > Java' 카테고리의 다른 글

BlockingQueue<E> vs ConcurrentHashMap<K, V>  (0) 2024.10.13
ConcurrentHashMap<K, V>  (1) 2024.10.03
String과 Char  (1) 2024.09.14
Integer.parseInt()란 무엇인가?  (2) 2024.09.08