동적으로 할당 된 어레이의 이상적인 성장률은 얼마입니까?


84

C ++에는 std :: vector가 있고 Java에는 ArrayList가 있으며 다른 많은 언어에는 고유 한 형태의 동적 할당 배열이 있습니다. 동적 배열에 공간이 부족하면 더 큰 영역에 재 할당되고 이전 값이 새 배열에 복사됩니다. 이러한 어레이의 성능에 대한 핵심적인 질문은 어레이의 크기가 얼마나 빨리 증가하는지입니다. 항상 현재 푸시에 맞을만큼만 커지면 매번 재 할당하게됩니다. 따라서 배열 크기를 두 배로 늘리거나 1.5x를 곱하는 것이 좋습니다.

이상적인 성장 인자가 있습니까? 2 배? 1.5 배? 이상적으로는 수학적으로 정당화되고 최상의 균형 성능과 낭비되는 메모리를 의미합니다. 이론적으로는 응용 프로그램에 잠재적 인 푸시 분포가있을 수 있다는 점을 감안할 때 이것이 다소 응용 프로그램에 따라 다릅니다. 하지만 "보통"최고인지 아니면 엄격한 제약 내에서 최고로 간주되는 가치가 있는지 궁금합니다.

어딘가에 이것에 대한 논문이 있다고 들었지만 찾을 수 없었습니다.

답변:


44

전적으로 사용 사례에 따라 다릅니다. 데이터 복사 (및 어레이 재 할당)에 낭비되는 시간이나 추가 메모리에 대해 더 신경 쓰십니까? 어레이는 얼마나 오래 지속됩니까? 오래 지속되지 않을 경우 더 큰 버퍼를 사용하는 것이 좋습니다. 페널티는 수명이 짧습니다. (예를 들어 Java에서, 이전 세대와 이전 세대로 이동하는 경우), 그것은 분명히 더 많은 벌칙입니다.

"이상적인 성장 인자"와 같은 것은 없습니다. 이론적으로 응용 프로그램에 의존하는 것이 아니라 확실히 응용 프로그램에 따라 다릅니다.

이 꽤 일반적인 성장 인자이다 - 나는 확신이 무엇이야 ArrayListList<T>의 .NET의 용도. ArrayList<T>Java에서는 1.5를 사용합니다.

편집 : Erich가 지적했듯이 Dictionary<,>.NET에서는 해시 값이 버킷간에 합리적으로 분산 될 수 있도록 "크기를 두 배로 늘리고 다음 소수로 증가"를 사용합니다. (최근에 프라임이 실제로 해시 버킷을 배포하는 데 그다지 좋지 않다는 문서를 보았을 것입니다.하지만 그것은 또 다른 대답에 대한 논쟁입니다.)


104

몇 년 전에 적어도 C ++에 적용된 것처럼 1.5가 2보다 선호되는 이유를 읽은 기억이납니다 (이는 런타임 시스템이 원하는대로 개체를 재배치 할 수있는 관리 언어에는 적용되지 않을 것입니다).

그 이유는 다음과 같습니다.

  1. 16 바이트 할당으로 시작한다고 가정 해 보겠습니다.
  2. 더 필요하면 32 바이트를 할당 한 다음 16 바이트를 확보합니다. 이로 인해 메모리에 16 바이트 구멍이 남습니다.
  3. 더 필요하면 64 바이트를 할당하여 32 바이트를 확보합니다. 이렇게하면 48 바이트 구멍이 남습니다 (16과 32가 인접 해있는 경우).
  4. 더 필요하면 128 바이트를 할당하여 64 바이트를 확보합니다. 이로 인해 112 바이트 구멍이 남습니다 (이전 할당이 모두 인접 해 있다고 가정).
  5. 기타 등등.

아이디어는 2 배 확장을 사용하면 결과 구멍이 다음 할당에 재사용 할 수있을만큼 충분히 커질 시점이 없다는 것입니다. 1.5x 할당을 사용하면 대신 다음과 같이됩니다.

  1. 16 바이트로 시작합니다.
  2. 더 많이 필요하면 24 바이트를 할당 한 다음 16 바이트를 확보하여 16 바이트 구멍을 남깁니다.
  3. 더 많이 필요하면 36 바이트를 할당 한 다음 24 바이트를 확보하고 40 바이트 구멍을 남깁니다.
  4. 더 필요하면 54 바이트를 할당 한 다음 36 바이트를 비우고 76 바이트 구멍을 남깁니다.
  5. 더 필요하면 81 바이트를 할당 한 다음 54 바이트를 확보하여 130 바이트의 구멍을 남깁니다.
  6. 더 필요하면 130 바이트 구멍에서 122 바이트 (반올림)를 사용합니다.

5
비슷한 이유를 찾은 무작위 포럼 게시물 ( objectmix.com/c/… ). 한 포스터는 (1 + sqrt (5)) / 2가 재사용의 상한선이라고 주장합니다.
Naaff

19
그 주장이 맞다면 phi (== (1 + sqrt (5)) / 2)는 실제로 사용하기에 최적의 숫자입니다.
Chris Jester-Young

1
이 답변은 1.5x 대 2x의 이론적 근거를 나타 내기 때문에 좋아하지만 Jon의 답변은 기술적으로 내가 말한 방식에 가장 적합합니다. 나는 왜 과거에 1.5가 추천되었는지 물었어야했다 : p
Joseph Garvin

6
Facebook은 FBVector 구현에서 1.5를 사용합니다. 여기 문서에서는 1.5가 FBVector에 최적 인 이유를 설명합니다.
csharpfolk 2014

2
@jackmott 맞습니다. 제 대답은 "이는 아마도 런타임 시스템이 원하는대로 개체를 재배치 할 수있는 관리 언어에는 적용되지 않을 것입니다"라고 말했습니다.
Chris Jester-Young

48

이상적 (같은 한계에 N → ∞), 그것의 황금 비율 : φ = 1.618은 ...

실제로 1.5와 같이 가까운 것을 원합니다.

그 이유는 캐싱을 활용하고 OS가 더 많은 메모리 페이지를 제공하지 않도록하기 위해 오래된 메모리 블록을 재사용 할 수 있기를 원하기 때문입니다. 이가 감소되도록 해결하려는 식 X N - 1 - 1 = X N + 1 - X N , 그 용액 접근 X = φ 위해 큰 N .


15

이와 같은 질문에 답할 때 한 가지 접근 방식은 널리 사용되는 라이브러리가 최소한 끔찍한 일을하지 않는다는 가정하에 인기있는 라이브러리를 "속임수"로보고 어떤 일을하는지 살펴 보는 것입니다.

따라서 매우 빠르게 확인하면 Ruby (1.9.1-p129)는 배열에 추가 할 때 1.5x를 사용하는 것으로 보이며 Python (2.6.2)은 1.125x와 상수 (in Objects/listobject.c)를 사용합니다.

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
    PyErr_NoMemory();
    return -1;
} else {
    new_allocated += newsize;
}

newsize위는 배열의 요소 수입니다. 에 newsize추가 new_allocated되므로 비트 시프트 및 삼항 연산자를 사용하는 표현식은 실제로 초과 할당을 계산하는 것입니다.


그래서 그것은 배열을 n에서 n + (n / 8 + (n <9? 3 : 6))로 성장시킵니다. 이것은 질문의 용어에서 성장 인자가 1.25x (더하기 상수)임을 의미합니다.
ShreevatsaR

1.125x 더하기 상수가 아닐까요?
Jason Creighton

10

배열 크기를 x. 따라서 size로 시작한다고 가정합니다 T. 다음에 배열을 확장하면 크기는 T*x. 그럼 그렇게 될 것 T*x^2입니다.

목표가 이전에 생성 된 메모리를 재사용하는 것이라면 할당 한 새 메모리가 할당 해제 한 이전 메모리의 합계보다 작은 지 확인해야합니다. 따라서 다음과 같은 불평등이 있습니다.

T*x^n <= T + T*x + T*x^2 + ... + T*x^(n-2)

양쪽에서 T를 제거 할 수 있습니다. 그래서 우리는 이것을 얻습니다 :

x^n <= 1 + x + x^2 + ... + x^(n-2)

비공식적으로 우리가 말하는 것은 nth할당시 이전에 할당 해제 된 메모리를 재사용 할 수 있도록 이전에 할당 해제 된 모든 메모리가 n 번째 할당에서 필요한 메모리보다 크거나 같아야한다는 것입니다.

예를 들어, 우리는 3 단계 (즉,에서이 작업을 수행 할 수 있도록하려면 n=3), 다음 우리가

x^3 <= 1 + x 

이 방정식은 0 < x <= 1.3(대략)

아래에서 다른 n에 대해 우리가 얻는 x를 참조하십시오.

n  maximum-x (roughly)

3  1.3

4  1.4

5  1.53

6  1.57

7  1.59

22 1.61

성장 요인은 2이후 보다 작아야 x^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2합니다.


두 번째 할당에서 1.5의 인수로 이전에 할당 해제 된 메모리를 이미 재사용 할 수 있다고 주장하는 것 같습니다. 이것은 사실이 아닙니다 (위 참조). 내가 당신을 오해하면 알려주세요.
awx 2013

두 번째 할당에서는 1.5 * 1.5 * T = 2.25 * T를 할당하고 그때까지 할 총 할당 해제는 T + 1.5 * T = 2.5 * T입니다. 따라서 2.5는 2.25보다 큽니다.
CEGRD 2013

아, 좀 더주의 깊게 읽어 보겠습니다. 당신이 말하는 모든 것은 할당 해제 된 총 메모리가 n 번째 할당에서 할당 된 메모리보다 많을 것이며 n 번째 할당에서 재사용 할 수 없다는 것 입니다.
awx 2013

4

정말 다릅니다. 어떤 사람들은 일반적인 사용 사례를 분석하여 최적의 수를 찾습니다.

나는 1.5x 2.0x phi x와 전에 사용 된 2의 거듭 제곱을 보았다.


피! 사용하기에 좋은 숫자입니다. 이제부터 사용을 시작하겠습니다. 감사! +1
Chris Jester-Young

이해가 안 돼요 ... 왜 파이? 이것에 적합한 속성은 무엇입니까?
Jason Creighton

4
@Jason : phi는 피보나치 수열을 만들기 때문에 다음 할당 크기는 현재 크기와 이전 크기의 합계입니다. 이것은 1.5보다 빠르지 만 2가 아닌 중간 속도의 성장을 허용합니다 (적어도 관리되지 않는 언어의 경우> = 2가 좋은 생각이 아닌 이유에 대한 내 게시물 참조).
Chris Jester-Young

1
@Jason : 또한 내 게시물의 댓글 작성자에 따르면 모든 숫자> phi는 사실 나쁜 생각입니다. 나는 이것을 확인하기 위해 수학을 직접하지 않았으므로 소금과 함께 가져 가십시오.
Chris Jester-Young

2

배열 길이에 따른 분포가 있고 공간 낭비와 시간 낭비를 비교하는 유틸리티 함수가있는 경우, 최적의 크기 조정 (및 초기 크기 조정) 전략을 확실히 선택할 수 있습니다.

단순 상수 배수가 사용되는 이유는 분명히 각 추가가 일정 시간을 상각하기 때문입니다. 그러나 이것이 작은 크기에 대해 다른 (더 큰) 비율을 사용할 수 없다는 의미는 아닙니다.

Scala에서는 현재 크기를 확인하는 함수로 표준 라이브러리 해시 테이블에 대한 loadFactor를 재정의 할 수 있습니다. 이상하게도 크기 조정이 가능한 배열은 두 배에 불과하며 대부분의 사람들이 실제로하는 일입니다.

나는 실제로 메모리 오류를 잡아 내고 그 경우에 덜 성장하는 배가 (또는 1.5 * ing) 배열을 알지 못합니다. 거대한 단일 어레이가 있다면 그렇게하고 싶을 것 같습니다.

또한 크기 조정이 가능한 어레이를 충분히 오래 유지하고 시간이 지남에 따라 공간을 선호한다면 처음에는 (대부분의 경우) 극적으로 오버로드 한 다음 정확히 올바른 크기로 재 할당하는 것이 합리적 일 수 있습니다. 끝난.


2

또 2 센트

  • 대부분의 컴퓨터에는 가상 메모리가 있습니다! 실제 메모리에서는 프로그램의 가상 메모리에서 단일 연속 공간으로 표시되는 임의의 페이지를 모든 곳에서 가질 수 있습니다. 간접적 해결은 하드웨어에 의해 수행됩니다. 가상 메모리 고갈은 32 비트 시스템에서 문제가되었지만 실제로는 더 이상 문제가되지 않습니다. 따라서 구멍을 채우는 것은 더 이상 문제가되지 않습니다 (특수 환경 제외). Windows 7부터 Microsoft도 추가 노력없이 64 비트를 지원합니다. @ 2011
  • 모든 r > 1 요인으로 O (1)에 도달합니다 . 동일한 수학적 증명은 매개 변수로 2에 대해서만 작동하지 않습니다.
  • r = 1.5는로 계산할 수 있으므로 old*3/2부동 소수점 연산이 필요하지 않습니다. ( /2컴파일러가 적합하다고 판단되면 생성 된 어셈블리 코드에서 비트 시프트로 대체 할 것이기 때문입니다.)
  • MSVC는 r = 1.5가되었으므로 2를 비율로 사용하지 않는 주요 컴파일러가 하나 이상 있습니다.

누군가가 언급했듯이 2는 8보다 기분이 좋습니다. 또한 2는 1.1보다 기분이 좋습니다.

내 느낌은 1.5가 좋은 기본값이라는 것입니다. 그 외에는 특정 경우에 따라 다릅니다.


3
n + n/2오버플로를 지연하는 데 사용 하는 것이 좋습니다 . 사용하면 n*3/2가능한 용량이 절반으로 줄어 듭니다.
owacoder

@owacoder 참. 그러나 n * 3이 맞지 않고 n * 1.5가 맞을 때 우리는 많은 메모리에 대해 이야기하고 있습니다. n이 32 비트 unsigend이면 n이 4G / 3 일 때 n * 3 오버플로, 즉 약 1.333G입니다. 엄청난 숫자입니다. 그것은 단일 할당에서 가질 수있는 많은 메모리입니다. 요소가 1 바이트가 아니라 예를 들어 각각 4 바이트이면 그 이상. 사용 사례에 대해 궁금해 ...
Notinlist 2019 년

3
엣지 케이스 일 수도 있지만 엣지 케이스는 일반적으로 물린 것입니다. 더 나은 디자인을 암시 할 수있는 가능한 오버플로 또는 기타 동작을 찾는 습관을 갖는 것은 현재로서는 어리석은 것처럼 보일지라도 결코 나쁜 생각이 아닙니다. 32 비트 주소를 예로 들어 보겠습니다. 이제 64가 필요합니다 ...
owacoder

0

나는 Jon Skeet의 의견에 동의합니다. 심지어 이론 제작자 친구조차도 계수를 2x로 설정할 때 이것이 O (1)임을 증명할 수 있다고 주장합니다.

CPU 시간과 메모리 사이의 비율은 각 컴퓨터마다 다르기 때문에 요인도 그만큼 다양합니다. 기가 바이트의 램이 있고 CPU가 느린 시스템이있는 경우 요소를 새 배열에 복사하는 것이 빠른 시스템보다 훨씬 더 비싸고 메모리가 적을 수 있습니다. 그것은 이론상으로 대답 할 수있는 질문입니다. 균일 한 컴퓨터라면 실제 시나리오에서는 전혀 도움이되지 않습니다.


2
자세히 설명하자면, 어레이 크기를 두 배로 늘리면 무모 화 O (1) 삽입물 을 얻게 됩니다. 아이디어는 요소를 삽입 할 때마다 이전 배열의 요소도 복사한다는 것입니다. 당신이 크기의 배열이 있다고 가정하자 미터 로, m의 그것의 요소. 요소 m + 1을 추가 할 때 공간이 없으므로 크기가 2m 인 새 배열을 할당합니다 . 처음 m 개 요소를 모두 복사하는 대신 새 요소를 삽입 할 때마다 하나씩 복사합니다. 이렇게하면 분산이 최소화되고 (메모리 할당을 위해 저장), 2m 요소를 삽입하면 이전 배열에서 모든 요소를 ​​복사하게됩니다.
hvidgaard 2014

-1

나는 그것이 오래된 질문이라는 것을 알고 있지만 모든 사람들이 놓친 것 같은 몇 가지가 있습니다.

사이즈 << 1 이것에 의해 승산된다 : 첫째,이 (2)에 의한 승산은 아무것도 1과 2 사이 : INT (플로트 (크기) * X), * 표시는 숫자 부동이다 X 소수점 연산하고, 상기 프로세서는 보유 float와 int 간의 캐스팅에 대한 추가 지침을 실행합니다. 즉, 기계 수준에서 두 배는 새로운 크기를 찾기 위해 매우 빠른 단일 명령을 필요로합니다. 1과 2 사이의 값을 곱하려면 최소한크기를 float로 캐스팅하는 명령어 하나, 곱할 명령어 하나 (float 곱셈이므로 4 배 또는 8 배가 아니더라도 적어도 두 배 더 많은 사이클이 필요함), int로 다시 캐스팅하는 명령어 하나, 플랫폼이 특수 레지스터를 사용하는 대신 범용 레지스터에서 부동 연산을 수행 할 수 있다고 가정합니다. 간단히 말해, 각 할당에 대한 수학은 간단한 왼쪽 이동보다 10 배 이상 오래 걸릴 것으로 예상해야합니다. 하지만 재 할당하는 동안 많은 데이터를 복사하는 경우 큰 차이가 없을 수 있습니다.

둘째, 그리고 아마도 큰 키커 일 것입니다. 모든 사람은 해제되는 메모리가 새로 할당 된 메모리와 인접 할뿐만 아니라 자체적으로도 연속적이라고 가정하는 것 같습니다. 모든 메모리를 미리 할당 한 다음 풀로 사용하지 않는 한 이것은 거의 확실하지 않습니다. OS 는 때때로하지만 대부분의 경우 충분한 여유 공간 조각화가 발생하여 적절한 메모리 관리 시스템이 메모리에 딱 맞는 작은 구멍을 찾을 수 있습니다. 일단 정말 비트 청크에 도달하면 연속적인 조각으로 끝날 가능성이 더 높지만 그때까지는 할당량이 충분히 커져 더 이상 중요하지 않을만큼 자주 수행하지 않습니다. 요컨대, 이상적인 숫자를 사용하면 여유 메모리 공간을 가장 효율적으로 사용할 수 있다고 상상하는 것은 재미 있지만 실제로는 프로그램이 베어 메탈에서 실행되지 않는 한 발생하지 않을 것입니다 (OS가없는 것처럼 그 아래에서 모든 결정을 내립니다).

질문에 대한 내 대답은? 아니요, 이상적인 숫자는 없습니다. 아무도 실제로 시도하지 않을 정도로 응용 프로그램에 따라 다릅니다. 당신의 목표가 이상적인 메모리 사용이라면, 당신은 거의 운이 없습니다. 성능을 위해 덜 빈번한 할당이 더 좋지만, 그렇게한다면 4 또는 8을 곱할 수 있습니다! 물론 파이어 폭스가 한 번에 1GB에서 8GB로 점프하면 사람들이 불평 할 것이므로 말도 안됩니다. 그래도 다음과 같은 몇 가지 경험 규칙이 있습니다.

메모리 사용을 최적화 할 수 없다면 최소한 프로세서주기를 낭비하지 마십시오. 2를 곱하는 것은 부동 소수점 수학을 수행하는 것보다 적어도 한 배 더 빠릅니다. 그것은 큰 차이를 만들지 않을 수도 있지만 적어도 약간의 차이를 만들 것입니다 (특히 더 빈번하고 작은 할당 동안).

그것을 지나치게 생각하지 마십시오. 이미 완료된 작업을 수행하는 방법을 알아 내기 위해 4 시간을 소비했다면 시간을 낭비한 것입니다. 솔직히 * 2보다 더 나은 옵션이 있다면 수십 년 전에 C ++ 벡터 클래스 (및 기타 여러 곳)에서 수행되었을 것입니다.

마지막으로, 정말로 최적화하고 싶다면 작은 일에 땀을 흘리지 마십시오. 이제는 임베디드 시스템에서 작업하지 않는 한 아무도 4KB의 메모리 낭비에 대해 신경 쓰지 않습니다. 각각 1MB에서 10MB 사이의 1GB 개체에 도달하면 두 배로 늘릴 수 있습니다 (즉, 100 개에서 1,000 개 사이의 개체). 예상 확장 률을 추정 할 수 있으면 특정 지점에서 선형 성장률로 평준화 할 수 있습니다. 분당 약 10 개의 개체를 예상한다면 단계 당 5 ~ 10 개의 개체 크기 (30 초에서 1 분에 한 번)로 성장하는 것이 좋습니다.

결론은 과도하게 생각하지 말고 가능한 것을 최적화하고 필요한 경우 애플리케이션 (및 플랫폼)에 맞게 사용자 정의하는 것입니다.


11
물론 n + n >> 1동일하다 1.5 * n. 당신이 생각할 수있는 모든 실질적인 성장 인자에 대해 비슷한 트릭을 만드는 것은 상당히 쉽습니다.
Björn Lindqvist

이것은 좋은 지적입니다. 그러나 ARM 외부에서는 이로 인해 명령어 수가 적어도 두 배가됩니다. (추가 명령어를 포함한 많은 ARM 명령어는 인수 중 하나에서 선택적 시프트를 수행 할 수 있으므로 예제가 단일 명령어에서 작동 할 수 있습니다.하지만 대부분의 아키텍처에서는이 작업을 수행 할 수 없습니다.) 아니요, 대부분의 경우 수를 두 배로 늘립니다. 1에서 2까지의 명령이 중요한 문제는 아니지만 수학이 더 복잡한 더 복잡한 성장 요인의 경우 민감한 프로그램의 성능 차이를 만들 수 있습니다.
Rybec Arethdar

@Rybec-하나 또는 두 개의 명령에 의한 타이밍 변화에 민감한 일부 프로그램이있을 수 있지만 동적 재 할당을 사용하는 프로그램이이를 염려 할 가능성은 거의 없습니다. 타이밍을 세밀하게 제어해야하는 경우에는 대신 정적으로 할당 된 스토리지를 사용하고있을 것입니다.
owacoder

나는 하나 또는 두 개의 지침이 잘못된 장소에서 상당한 성능 차이를 만들 수있는 게임을합니다. 즉, 메모리 할당이 잘 처리되면 몇 가지 명령이 차이를 만들만큼 자주 발생하지 않아야합니다.
Rybec Arethdar 2019
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.