Java/Java

Java - 배열 복사와 반복, clone()과 arraycopy() 속도 비교

Dlise 2023. 11. 21. 17:40

코딩테스트 문제를 풀던 중 배열을 여러 번 반복해야 하는 상황이 생겼다.

String 자료형의 경우 repeat 메서드를 활용하면 되지만 배열은 이런 메서드가 없다.

 

해당 정보를 찾을 겸 배열의 복사와 반복에 대해 내용을 정리하고자 한다.

 

얕은 복사(Shallow Copy)

복사라고 말하기도 애매한 경우이다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = arr;
    }
}

 

int[] arr = {1, 2, 3, 4, 5}; 코드를 통해 메모리 heap 영역에 배열 데이터가 저장된다. 

변수명 arr은 배열 데이터의 주소 정보를 가지고 있다.

 

int[] copyArr = arr; 코드를 통해 arr이 가지고 있는 주소 정보를 copyArr도 가진다.

즉, copyArr과 arr 변수는 동일한 배열을 가리키는 것이다. 이런 경우를 얕은 복사라고 한다.

 

실제로 아래 코드를 통해 주소를 출력하면 같은 값인 것을 확인할 수 있다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = arr;

        System.out.println("arr:\t" + System.identityHashCode(arr));
        System.out.println("copyArr:" + System.identityHashCode(copyArr));
    }
}

 

가리키고 있는 주소가 동일하기 때문에 배열 값이 바뀌는 경우 두 변수 모두 출력 결과가 바뀐다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = arr;
        System.out.println("(before)arr: \t" + Arrays.toString(arr));
        System.out.println("(before)copyArr:" + Arrays.toString(copyArr));

        copyArr[0] = 6;
        System.out.println("(after)arr: \t" + Arrays.toString(arr));
        System.out.println("(after)copyArr: " + Arrays.toString(copyArr));
    }
}

이 방법을 활용하기엔 잠재적인 문제가 많으니 다른 방법을 찾아보자.

 

Arrays.copyOf, Arrays.copyOfRange

Java Arrays 클래스엔 배열 복사 기능을 제공하는 copyOfcopyOfRange 메서드가 있다.

배열의 자료형에 따라 오버로딩 되어있는 모습이다.

 

Arrays.copyOf

먼저 copyOf 먼저 살펴보자. 

copyOf 메서드는 배열(original)과 int 자료형(newLength)을 매개변수로 가진다.

newLength 복사하려는 길이이다.

 

아래 코드를 보면 바로 이해가 될 것이다.

이번에는 얕은 복사가 아니라 Arrays.copyOf를 활용해 copyArr에 내용을 담았다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = Arrays.copyOf(arr, 5);

        System.out.println("arr:\t" + System.identityHashCode(arr));
        System.out.println("copyArr:" + System.identityHashCode(copyArr) + "\n");

        System.out.println("(before)arr: \t" + Arrays.toString(arr));
        System.out.println("(before)copyArr:" + Arrays.toString(copyArr));

        copyArr[0] = 6;
        System.out.println("(after)arr: \t" + Arrays.toString(arr));
        System.out.println("(after)copyArr: " + Arrays.toString(copyArr));
    }
}

arr과 copyArr이 가리키는 주소 정보가 다른 것을 확인할 수 있다.

 

이처럼 값만 가져와 다른 객체를 만드는 것을 깊은 복사라고 한다.

copyArr[0] = 6;을 통해 배열 값을 바꿔도 arr엔 영향을 미치지 않는다.

 

만약 newLength의 값을 바꾸면 그에 따라 복사된 배열의 길이가 바뀐다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = Arrays.copyOf(arr, 4);
        
        System.out.println("(before)arr: \t" + Arrays.toString(arr));
        System.out.println("(before)copyArr:" + Arrays.toString(copyArr));
    }
}

 

newLength에 기존 배열보다 더 큰 값을 넘기면 그만큼은 default 값으로 저장된다.

public class Main {
    public static void main(String[] args) {
        int[] arrInt = {1, 2, 3, 4, 5};
        boolean[] arrBoolean = {true, true};

        int[] copyArrInt = Arrays.copyOf(arrInt, 8);
        boolean[] copyArrBoolean = Arrays.copyOf(arrBoolean, 5);

        System.out.println("copyArrInt:    " + Arrays.toString(copyArrInt));
        System.out.println("copyArrBoolean:" + Arrays.toString(copyArrBoolean));
    }
}

 

int형 배열은 0, boolean형 배열은 false로 채워진 것을 볼 수 있다.

 

아래는 Arrays 클래스의 copyOf 메서드이다. 내부적으로 System.arraycopy를 활용한다. 

 

 

Arrays.copyOfRange

copyOf는 0번 인덱스부터 newLength만큼 복사하는데, 이와 달리 copyOfRange는 복사할 범위를 정할 수 있다.

newLength대신 fromto가 있다. copyOf와 동작이 유사하니 바로 확인해 보자.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = Arrays.copyOfRange(arr, 1, 3);

        System.out.println("arr:    " + Arrays.toString(arr));
        System.out.println("copyArr:" + Arrays.toString(copyArr));
    }
}

from엔 1, to엔 3을 보냈는데 결과로 arr[1]부터 arr[2]까지 복사가 되었다.

여기서 알 수 있듯이 (to로 넘긴 숫자) - 1까지만 복사한다.

 

만약 to의 값이 복사하려는 배열의 길이보다 길면 default값으로 채워진다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = Arrays.copyOfRange(arr, 3, 10);

        System.out.println("arr:    " + Arrays.toString(arr));
        System.out.println("copyArr:" + Arrays.toString(copyArr));
    }
}

 

아래는 Arrays 클래스의 copyOfRange 메서드이다. 내부적으로 System.arraycopy를 활용한다. 

만약 from의 값이 to의 값보다 크면 IllegalArgumentException을 발생시킨다.

 

 

System.arraycopy

이제 copyOf와 copyOfRange에서 사용하는 System.arraycopy에 대해 알아보자.

System 클래스의 arraycopy는 아래의 매개변수를 가진다.

풀어서 설명하자면 'src의 srcPos부터 length만큼 dest의 destPos부터 복사한다.'이다.

 

바로 코드를 보자.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = new int[10];
        System.arraycopy(arr, 0, copyArr, 3, 5);

        System.out.println("arr:    " + Arrays.toString(arr));
        System.out.println("copyArr:" + Arrays.toString(copyArr));
    }
}

 

System.arrayCopy(arr, 0, copyArr, 3, 5); 코드를 통해 배열을 깊은 복사 했다.

arr의 0번 인덱스부터 5의 길이만큼 copyArr의 3번 인텍스부터 복사한다는 것이다.

 

따라서 결과를 보면 copyArr[3]부터 저장된 것을 볼 수 있다.

인자 값을 바꿈으로써 원하는 배열에 원하는 범위만큼 원하는 위치에 복사할 수 있다.

 

아래는 arraycopy의 코드로 @IntrinsicCandidate 어노테이션을 가지고 있다.

 

@IntrinsicCandidate에 대한 설명은 아래 나와있다.

https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/jdk/internal/vm/annotation/IntrinsicCandidate.java

컴파일러 단에서 동작해서 좋은 성능을 보인다는 것 같은데 정확하게는 모르겠다.

 

 

Object.clone

다음은 Object 클래스의 clone이다. 이 또한 깊은 복사이며 활용이 간단하다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = arr.clone();
        
        System.out.println("arr:    " + System.identityHashCode(arr));
        System.out.println("arr:    " + System.identityHashCode(copyArr));
        
        System.out.println("arr:    " + Arrays.toString(arr));
        System.out.println("copyArr:" + Arrays.toString(copyArr));
    }
}

 

아래는 clone() 코드인데 여기에도 @IntrinsicCandidate 어노테이션이 붙어있다.

 

 

Object.clone() vs System.arraycopy()

그렇다면 위 둘 중에 무엇이 더 빠를까? 궁금해서 바로 확인해 보았다.

먼저 clone()의 시간을 확인해 보자.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[][] copyArr = new int[10000000][5];

        long beforeTime = System.nanoTime();
        for(int i = 0; i < 10000000; i++) 
            copyArr[i] = arr.clone();
        
        long afterTime = System.nanoTime();
        System.out.println((afterTime - beforeTime));
    }
}

 

5회 실행한 결과는 아래와 같다.(단위: nanosecond)

370,125,300

366,085,000

330,343,800

367,717,500

323,731,500

 

다음은 arraycopy()를 확인해 보자.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[][] copyArr = new int[10000000][5];

        long beforeTime = System.nanoTime();
        for(int i = 0; i < 10000000; i++) 
            System.arraycopy(arr, 0, copyArr[i], 0, 5);
        
        long afterTime = System.nanoTime();
        System.out.println((afterTime - beforeTime));
    }
}

 

5회 실행한 결과는 아래와 같다. (단위: nanosecond)

62,830,300

64,003,300

56,266,100

62,924,000

55,762,100

 

아래는 둘의 평균을 비교한 결과이다. (단위: nanosecond)

Object.clone(): 351,600,620

System.arraycopy(): 60,357,160

 

arraycopy()가 월등히 빠르다.

 

배열의 반복

그렇다면 이제 원래 목적이었던 배열을 복사해 반복을 해보자!

가장 성능이 좋은 arraycopy()를 활용할 것이다.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int[] copyArr = repeatArr(arr, 3);
        
        System.out.println(Arrays.toString(copyArr));
    }

    static int[] repeatArr(int[] original, int repeatCount) {
        int[] arr = new int[original.length * repeatCount];
        for(int i = 0; i < repeatCount; i++) {
            System.arraycopy(original, 0, arr, i * original.length, 5);
        }
        return arr;
    }
}

구현 완료!

 

배열의 반복을 지원하는 API가 있을 것 같긴 한데 찾지 못했다. 만약 발견한다면 추가할 계획이다.