Uknow's Lab.
article thumbnail

 

https://uknowblog.tistory.com/157

 

[백준 15688번] [Java] 수 정렬하기 5

https://www.acmicpc.net/problem/15688 15688번: 수 정렬하기 5 첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄부터 N개의 줄에는 숫자가 주어진다. 이 수는 절댓값이 1,000,000보다 작거나 같은 정

uknowblog.tistory.com

 

백준 15688번, 수 정렬하기 5 문제를 풀다가 미처 생각하지 못했던 부분을 하나 알게되었습니다.

 

 

 

StringBuilder의 개행문자 추가 - sb.append(str + "\n") VS sb.append(str).append("\n")

 

// 통과한 코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

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

        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }
        Arrays.sort(arr);

        StringBuilder sb = new StringBuilder();

        for(int i = 0; i<n; i++) {
            sb.append(arr[i]).append("\n");
        }

        System.out.print(sb);
    }
}



// 시간초과가 난 코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

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

        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }
        Arrays.sort(arr);

        StringBuilder sb = new StringBuilder();

        for(int i = 0; i<n; i++) {
            sb.append(arr[i] + "\n");
        }

        System.out.print(sb);
    }
}

 

15688번 문제는 수 정렬하기 문제로써,

자바의 기본 정렬 메소드를 사용해 풀이했어요.

 

위 코드는 통과한 코드, 아래 코드는 시간초과를 받은 코드인데,

두 코드의 차이점이 보이시나요?

 

위 코드)            sb.append(arr[i]).append("\n");

아래 코드)         sb.append(arr[i] + "\n");

 

다른 곳은 오직 sb에 개행문자를 추가하는 방법 하나 뿐 입니다.

보통의 문제였다면 개행문자 추가 방식은 시간초과를 가를 만큼 유의미한 요소는 아니겠지만,

해당 문제는 누적시간이라는, 매우 타이트한 시간을 갖고 있는 문제로써

개행문자 추가 방식이 시간초과를 가를 만큼 유의미했던거죠.

 

처음에 저는, StringBuilder에 문자열을 추가할 때는

sb.append(str).append("\n")와 같이 쓰면 append가 두 번 호출되기 때문에

불필요한 리소스 낭비라 생각하여 sb.append(str + "\n")과 같이 사용하였으나,

실제로는 sb.append(str).append("\n")이 더 효율적으로 작동하고 있었어요.

 

궁금해진 저는 바로 Intellij를 키고 실험을 해보기로 했습니다.

 


 

직접 실험해보기

 

실험 방법은 다음과 같습니다.

  1. sb.append("str" + "\n") 방식
    1. 시작시간을 저장한다
    2. sb.append("str" + "\n") 방식으로 대량의 데이터를 삽입한다
    3. 종료 시간을 저장한다
  2. sb.append("str").append("\n") 방식
    1. 시작시간을 저장한다
    2. sb.append("str").append("\n") 방식으로 대량의 데이터를 삽입한다
    3. 종료시간을 저장한다
  3. 두 방식의 각각의 수행시간 (종료시간 - 시작시간)을 비교한다.

 

 

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

        int caseCnt = 50000000;

        // case 1 - sb.append(str + "\n")
        long case1StartTime = System.currentTimeMillis();

        StringBuilder sb1 = new StringBuilder();
        for (int i = 0; i < caseCnt; i++) {
            sb1.append(i + "\n");
        }
        long case1EndTime = System.currentTimeMillis();


        // case 2 - sb.append(str).append("\n")
        long case2StartTime = System.currentTimeMillis();

        StringBuilder sb2 = new StringBuilder();
        for (int i = 0; i < caseCnt; i++) {
            sb2.append(i).append("\n");
        }
        long case2EndTime = System.currentTimeMillis();


        System.out.println(case1EndTime - case1StartTime);
        System.out.println(case2EndTime - case2StartTime);
    }
}

코드는 위와 같습니다.

 

실험결과

 

 

case 1 - sb.append("str" + "\n") 의 방식은 1674ms

case 2 - sb.append("str").append("\n")의 방식은 962ms로,

개행문자를 따로 append("\n") 해주는 방식이 더 빨랐습니다.

 


이유가 뭘까.

 

생각해보니 sb.append("str" + "\n"); 과 같이 쓰면

str과 \n 문자열을 합쳐 새로운 문자열을 생성해 append하는 방식입니다.

 

str1 + str2 과 같이, 문자열을 합치려면 꽤 시간이 들기 때문에,

문자열을 + 연산으로 합치지 않기 위해 StringBuilder()에 append하여 사용하는 건데,

str + "\n"과 같이 사용해버리면, append에 인자로 넘겨주기 전에 새 문자열을 생성해 넘겨주게 됩니다.

 

sb.append(str).append("\n")가 append를 한 번 더 호출해 불필요한 자원을 낭비할거라 생각했는데,

오히려 + 연산으로 자원을 더 낭비하고 있던 셈이죠.

문자열을 합치는 과정에서 두 문자열의 내용을 복사하는 것과, 새 객체를 생성하는 과정.

생각해보니 결코 라이트하지 않습니다.

 

문자열을 +로 이어붙이는걸 피하기 위해 StringBuilder와 append를 쓴다.

이것만 생각하면 매우 간단하고 당연한 것이였네요...

 


 

그럼 코틀린의 문자열 템플릿은?

 

코틀린에는 문자열에 $기호를 사용해 템플릿처럼 사용할 수 있는 기능이 있습니다.

sb.append("${str}\n")과 같이 사용한다면,

이 경우에도 + 연산처럼 더 낮은 퍼포먼스를 보일까요?

fun main() {

    val caseCnt = 50000000

    // case 1 - sb.append(str + "\n")

    // case 1 - sb.append(str + "\n")
    val case1StartTime = System.currentTimeMillis()

    val sb1 = StringBuilder()
    for (i in 0 until caseCnt) {
        sb1.append("${i}\n")
    }
    val case1EndTime = System.currentTimeMillis()


    // case 2 - sb.append(str).append("\n")


    // case 2 - sb.append(str).append("\n")
    val case2StartTime = System.currentTimeMillis()

    val sb2 = StringBuilder()
    for (i in 0 until caseCnt) {
        sb2.append(i).append("\n")
    }
    val case2EndTime = System.currentTimeMillis()


    println(case1EndTime - case1StartTime)
    println(case2EndTime - case2StartTime)
}

 

 

당연하게도 그렇습니다.

템플릿도 결국은 문자열을 중간에 넣어 새로운 문자열을 만드는 것이므로,

append("\n")로 개행문자를 따로 append 하여

문자열을 합치는 과정을 최소로 하는게 더 좋은 퍼포먼스를 보입니다.

profile

Uknow's Lab.

@유노 Uknow

인생은 Byte와 Double 사이 Char다. 아무말이나 해봤습니다.