좋은 C 가변 길이 배열 예제 [닫기]


9

이 질문은 SO에서 얼어 붙은 수신을 얻었으므로 거기에서 삭제하고 대신 시도해보기로 결정했습니다. 여기에 맞지 않다고 생각되면 적어도 내가 본 예제를 찾는 방법에 대한 의견을 남겨주십시오 ...

C99 VLA를 사용하면 현재 표준 힙 사용 C ++ RAII 메커니즘과 비교하여 실질적인 이점 을 제공하는 예를들 수 있습니까 ?

내가 따르는 예는 다음과 같습니다.

  1. 힙 사용에 비해 쉽게 측정 가능한 (10 %) 성능 이점을 달성하십시오.
  2. 전체 배열이 전혀 필요하지 않은 좋은 해결 방법이 없습니다.
  3. 고정 된 최대 크기 대신 동적 크기를 사용하면 실제로 이점이 있습니다.
  4. 일반적인 사용 시나리오에서 스택 오버플로가 발생하지 않을 것입니다.
  5. C ++ 프로젝트에 C99 소스 파일을 포함시키기 위해 성능이 필요한 개발자를 유혹 할 수있을 정도로 강력해야합니다.

컨텍스트에 대한 설명 추가 : C99가 의미하는 VLA를 의미하며 표준 C ++에는 포함되어 있지 않습니다. int array[n]여기서는 n변수입니다. 그리고 다른 표준 (C90, C ++ 11)에서 제공하는 대안보다 우선하는 사용 사례의 예를 봅니다.

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

몇 가지 아이디어 :

  • varargs를 사용하는 함수는 항목 수를 자연스럽게 합리적으로 제한하지만 유용한 API 수준 상한은 없습니다.
  • 낭비되는 스택이 바람직하지 않은 재귀 함수
  • 힙 오버 헤드가 나쁜 많은 작은 할당 및 릴리스.
  • 성능이 중요하고 작은 함수가 많이 인라인 될 것으로 예상되는 임의 크기의 행렬과 같은 다차원 배열을 처리합니다.
  • 주석에서 : 동시 알고리즘, 힙 할당에는 동기화 오버 헤드가 있습니다.

Wikipedia에는 내 기준을 충족시키지 못하는 예가 있습니다. 힙을 사용하는 것과의 실질적인 차이는 컨텍스트가 없으면 관련이 없기 때문입니다. 더 많은 컨텍스트가 없으면 항목 수가 스택 오버플로를 일으킬 수있는 것처럼 보이기 때문에 이상적이지 않습니다.

참고 : 나는 예제를 직접 구현하기 위해 예제 코드 또는 이것으로부터 이익을 얻을 수있는 알고리즘을 제안합니다.


1
약간의 추측은 (손톱을 찾는 망치이기 때문에) 아마도 후자잠금 경합 때문에 멀티 스레드 환경에서 alloca()실제로 빛날 것 입니다 . 그러나 작은 배열은 고정 된 크기를 사용해야하며 큰 배열에는 어쨌든 힙이 필요할 수 있기 때문에 이것은 실제로 확장입니다. malloc()
chrisaycock

1
@ chrisaycock 네, 손톱을 찾고있는 망치는 많지만 실제로 존재하는 망치입니다 (C99 VLA 또는 실제로는 표준 alloca이 아닙니다. 기본적으로 동일하다고 생각합니다). 그러나 그 멀티 스레드 된 것은 훌륭합니다. 편집 질문이 포함되어 있습니다!
hyde

VLA의 한 가지 단점은 할당 실패를 감지 할 수있는 메커니즘이 없다는 것입니다. 메모리가 충분하지 않으면 동작이 정의되지 않습니다. (고정 크기 배열과 alloca ()의 경우도 마찬가지입니다.)
Keith Thompson

@KeithThompson 음, malloc / new가 할당 실패를 감지한다고 보장 할 수는 없습니다. 예를 들어 Linux malloc 매뉴얼 페이지 ( linux.die.net/man/3/malloc )를 참조하십시오 .
hyde

@hyde : 그리고 Linux의 malloc동작이 C 표준을 따르는 지 여부는 논쟁의 여지가 있습니다.
Keith Thompson

답변:


9

방금 같은 시드에서 다시 시작하는 임의의 숫자 세트를 생성하는 작은 프로그램을 해킹하여 "공정한"및 "비교할 수있는"것을 보장했습니다. 진행하면서이 값의 최소값과 최대 값을 알아냅니다. 이 숫자의 집합을 생성 한 때, 그것은 평균보다 얼마나 많은 계산 min하고 max.

매우 작은 어레이의 경우 VLA 이상의 장점이 std::vector<>있습니다.

실제 문제는 아니지만 임의의 숫자를 사용하는 대신 작은 파일에서 값을 읽고 동일한 종류의 코드로 더 의미있는 카운트 / 최소 / 최대 계산을 수행하는 것을 쉽게 상상할 수 있습니다. .

관련 함수에서 "임의의 수"(x)의 매우 작은 값의 경우 vla솔루션이 큰 차이로 이깁니다. 크기가 커질수록 "승리"가 작아지고 충분한 크기가 주어지면 벡터 솔루션이 더 효율적으로 보입니다. VLA에서 수천 개의 요소를 가질 때와 같이 변형을 너무 많이 연구하지는 않았습니다. 정말로 그들이해야 할 일 ...

그리고 누군가이 템플릿을 사용 하여이 모든 코드를 작성하는 방법이 있다고 말할 것이라고 확신 cout합니다. 런타임에 RDTSC와 비트 이상을 실행하지 않고도이 작업을 수행 할 수 있습니다 ...하지만 실제로는 그렇지 않습니다. 요점.

이 특정 변형을 실행할 때 func1(VLA)와 func2(std :: vector) 사이에 약 10 %의 차이가 발생합니다 .

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

이것은 다음과 같이 컴파일됩니다 : g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

코드는 다음과 같습니다.

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

와우, 내 시스템의 VLA 버전이 30 % 이상 향상되었습니다 std::vector.
chrisaycock

1
음, 20-200 대신 5-15 정도의 크기 범위로 시도하면 1000 % 이상 개선 될 수 있습니다. [또한 컴파일러 옵션에 따라 다릅니다-gcc에 컴파일러 옵션을 표시하기 위해 위의 코드를 편집하겠습니다]
Mats Petersson

방금 대신 func3사용 하고의 필요성을 제거하는를 추가했습니다 . 를 사용하는 것보다 약 10 % 더 오래 걸립니다 . [물론, 그 과정 에서 함수의 사용이 시간이 걸리는 데 크게 기여한다는 것을 알았습니다. 조금 놀랐습니다.] v.push_back(rand())v[i] = rand();resize()resize()v[i]
Mats Petersson 2014 년

1
@MikeBrown std::vectorVLA /를 사용 하는 실제 구현을 알고 alloca있습니까, 아니면 단지 추측일까요?
hyde

3
벡터는 실제로 내부적으로 배열을 사용하지만 내가 아는 한 VLA를 사용할 방법이 없습니다. 필자의 예는 VLA가 데이터 양이 적은 일부 (아마도 많은 경우)에 유용하다고 생각합니다. 벡터가 VLA를 사용하더라도 vector구현 내부에서 추가 노력을 기울여야합니다 .
Mats Petersson 2016 년

0

VLA 및 벡터 관련

Vector가 VLA 자체를 활용할 수 있다고 생각 했습니까? VLA가 없으면 Vector는 저장을 위해 10, 100, 10000과 같이 배열의 특정 "스케일"을 지정해야하므로 101 개의 항목을 보유하도록 10000 개의 항목 배열을 할당하게됩니다. VLA를 사용하여 200으로 크기를 조정하면 알고리즘은 200 만 필요하고 200 개의 항목 배열을 할당 할 수 있다고 가정 할 수 있습니다. 또는 n * 1.5의 버퍼를 할당 할 수 있습니다.

어쨌든 런타임에 필요한 항목 수를 알고 있으면 VLA가 더 성능이 뛰어납니다 (매트의 벤치 마크가 시연 한 것처럼). 그가 보여준 것은 간단한 2 패스 반복이었습니다. 무작위 샘플이 반복적으로 수집되는 몬테 카를로 시뮬레이션 또는 각 요소에 대해 여러 번 계산이 수행되는 이미지 조작 (Photoshop 필터와 같은) 및 각 요소에 대한 각 계산에는 이웃을 보는 것이 포함됩니다.

추가 포인터가 벡터에서 내부 배열로 점프합니다.

주요 질문에 답변

그러나 LinkedList와 같이 동적으로 할당 된 구조를 사용하는 것에 대해서는 비교할 필요가 없습니다. 배열은 해당 요소에 대한 포인터 산술을 사용하여 직접 액세스 할 수 있습니다. 링크 된 목록을 사용하면 특정 요소에 도달하기 위해 노드를 걸어야합니다. 따라서이 시나리오에서 VLA가 승리합니다.

이 답변 에 따르면 아키텍처에 따라 다르지만 일부 경우에는 스택이 캐시에서 사용 가능하기 때문에 스택의 메모리 액세스가 더 빠릅니다. 많은 수의 요소를 사용하면 이것이 무시 될 수 있습니다 (잠재적으로 Mats가 벤치 마크에서 줄어든 수익의 원인). 그러나 캐시 크기가 크게 증가하고 있으며 그에 따라 더 많은 수를 증가시킬 가능성이 있습니다.


링크 된 목록에 대한 귀하의 참조를 이해하지 못하므로 질문에 섹션을 추가하여 컨텍스트를 조금 더 설명하고 내가 생각하는 대안의 예를 추가했습니다.
hyde

std::vector스케일이 필요한가? 101 개만 필요할 때 10K 요소를위한 공간이 필요한 이유는 무엇입니까? 또한 질문에는 링크 된 목록이 언급되어 있지 않으므로 어디서 가져 왔는지 확실하지 않습니다. 마지막으로 C99의 VLA는 스택 할당됩니다. 그것들은 표준 형식입니다 alloca(). 힙 스토리지 (함수가 리턴 된 후 realloc()유지됨 ) 또는 (어레이 크기 조정)가 필요한 것은 VLA를 금지합니다.
chrisaycock

@chrisaycock C ++에는 메모리가 new []로 할당되었다고 가정하면 어떤 이유로 realloc () 함수가 부족합니다. 이것이 std :: vector가 스케일을 사용해야하는 이유가 아닌가?

@Lundin C ++은 10의 거듭 제곱으로 벡터를 확장합니까? 방금 연결된 목록 참조를 고려할 때 Mike Brown 이이 질문에 실제로 혼란 스럽다는 인상을 받았습니다. (그는 또한 C99 VLA가 힙에 있다는 것을 암시하는 이전 주장을했다.)
chrisaycock

@ hyde 나는 그것이 당신이 말한 것을 깨닫지 못했습니다. 다른 힙 기반 데이터 구조를 의미한다고 생각했습니다. 이 설명을 추가 했으므로 흥미 롭습니다. 나는 C ++ 괴짜가 충분하지 않아서 그 차이점을 말할 수 없습니다.
Michael Brown

0

VLA를 사용하는 이유는 주로 성능입니다. 위키 예제를 "관련이없는"차이로만 무시하는 것은 실수입니다. 예를 들어, 함수가 타이트한 루프에서 호출 된 경우 read_val, 속도가 중요한 일부 시스템에서 매우 빠르게 반환되는 IO 함수 인 경우와 같이 정확히 해당 코드에 큰 차이가있을 수있는 경우를 쉽게 확인할 수 있습니다 .

실제로 VLA가 이러한 방식으로 사용되는 대부분의 장소에서는 힙 호출을 대체하지 않고 대신 다음과 같이 대체합니다.

float vals[256]; /* I hope we never get more! */

지역 선언에 관한 것은 매우 빠르다는 것입니다. 이 줄은 float vals[n]일반적으로 몇 개의 프로세서 명령 만 필요합니다 (단 하나만 가능). n스택 포인터에 값을 추가하기 만하면 됩니다.

반면, 힙 할당은 빈 영역을 찾기 위해 데이터 구조를 걷는 것이 필요합니다. 가장 운이 좋은 경우에도 시간이 훨씬 길어질 수 있습니다. (즉 n, 스택 에 배치 하고 호출하는 작업 malloc은 아마도 5-10 개의 명령 일 것입니다.) 힙에 적당한 양의 데이터가 있으면 아마도 훨씬 더 나쁩니다. malloc실제 프로그램에서 100x에서 1000x까지 느린 경우 를 보더라도 전혀 놀랍지 않습니다 .

물론, 당신은 또한 호출 free과 규모가 비슷한 매칭으로 약간의 성능 영향을 미칩니다 malloc.

또한 메모리 조각화 문제가 있습니다. 많은 작은 할당이 힙을 조각화하는 경향이 있습니다. 조각난 힙은 메모리를 낭비하고 메모리 할당에 필요한 시간을 늘립니다.


Wikipedia 예 : 좋은 예의 일부 일 수는 있지만 상황에 따라 더 많은 코드가 없으면 내 질문에 열거 된 5 가지 사항이 실제로 표시 되지 않습니다 . 그렇지 않으면, 나는 당신의 설명에 동의합니다. 명심해야 할 것이 있지만 VLA를 사용하면 로컬 변수에 액세스하는 데 비용이들 수 있으며 컴파일 할 때 모든 로컬 변수의 오프셋을 반드시 알 필요는 없으므로 일회성 힙 비용을 모든 반복에 대한 내부 루프 패널티.
hyde

음 ... 무슨 뜻인지 모르겠습니다. 지역 변수 선언은 단일 작업이며 약간 최적화 된 컴파일러는 내부 루프에서 할당을 가져옵니다. 지역 변수에 액세스하는 데 특별한 "비용"은 없으며 VLA가 증가하는 것은 아닙니다.
로봇 고트

구체적인 예 : int vla[n]; if(test()) { struct LargeStruct s; int i; }: s컴파일시 스택 오프셋을 알 수 없으며 컴파일러가 i내부 범위 외부의 스토리지 를 고정 스택 오프셋으로 옮길 지 의심됩니다 . 따라서 간접적 인 때문에 추가 기계 코드가 필요하며 이는 PC 하드웨어에서 중요한 레지스터를 소모 할 수도 있습니다. 컴파일러 어셈블리 출력이 포함 된 예제 코드를 원하면 별도의 질문을하십시오.)
hyde

컴파일러는 코드에서 발견 된 순서대로 할당 할 필요가 없으며 공간이 할당되어 사용되지 않는지는 중요하지 않습니다. 스마트 최적화를위한 공간을 할당 할 si기능을 입력 할 때, 이전에 test호출되거나 vla할당에 대한 할당으로, s그리고 i부작용이 없습니다. (실제로, i"할당"이 전혀 없음을 의미하는 레지스터에도 배치 될 수 있습니다.) 스택의 할당 순서 또는 스택 사용에 대한 컴파일러 보증은 없습니다.
로봇 고트

(멍청한 실수로 잘못된 의견을 삭제)
hyde
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.