C 및 C ++와 같은 언어에서 변수에 대한 포인터를 사용하는 동안 해당 주소를 저장하려면 하나 이상의 메모리 위치가 필요합니다. 이것이 메모리 오버 헤드가 아닙니까? 이것은 어떻게 보상됩니까? 시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
C 및 C ++와 같은 언어에서 변수에 대한 포인터를 사용하는 동안 해당 주소를 저장하려면 하나 이상의 메모리 위치가 필요합니다. 이것이 메모리 오버 헤드가 아닙니까? 이것은 어떻게 보상됩니까? 시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
답변:
실제로, 오버 헤드는 실제로 포인터를 저장하는 데 필요한 여분의 4 또는 8 바이트에 있지 않습니다. 대부분 포인터는 동적 메모리 할당에 사용됩니다 . 즉, 메모리 블록을 할당하는 함수를 호출하면이 함수는 해당 메모리 블록을 가리키는 포인터를 반환합니다. 이 새로운 블록 자체는 상당한 오버 헤드를 나타냅니다.
지금, 당신은하지 않습니다 해야 당신의 배열을 가질 수 있습니다 포인터를 사용하기 위해 메모리 할당에 참여 int
정적 또는 스택에 선언을, 당신은 방문하는 인덱스 대신 포인터를 사용할 수 int
들, 그것은이다 아주 좋고 간단하고 효율적입니다. 메모리 할당이 필요하지 않으며 포인터는 일반적으로 정수 인덱스처럼 메모리의 공간을 정확히 차지합니다.
또한 Joshua Taylor가 코멘트에서 우리에게 생각 나듯이 포인터는 참조로 무언가를 전달하는 데 사용됩니다. 예 struct foo f; init_foo(&f);
를 들어 스택에 f를 할당 한 다음 init_foo()
포인터를 호출 하여 호출 합니다 struct
. 매우 흔합니다. (그러한 포인터를 "위로"전달하지 않도록주의하십시오.) C ++에서는 foo&
포인터 대신 "참조"( )를 사용 하여이 작업이 수행되는 것을 볼 수 있지만 참조는 변경하지 않는 포인터 일 뿐이며 같은 양의 메모리.
그러나 포인터가 사용되는 주된 이유는 동적 메모리 할당을위한 것이므로 다른 방법으로는 해결할 수없는 문제를 해결하기 위해 수행됩니다. 다음은 간단한 예입니다. 파일의 전체 내용을 읽고 싶다고 상상해보십시오. 그것들을 어디에 보관하겠습니까? 고정 크기 버퍼로 시도하면 해당 버퍼보다 길지 않은 파일 만 읽을 수 있습니다. 그러나 메모리 할당을 사용하면 파일을 읽는 데 필요한만큼의 메모리를 할당 한 다음 계속해서 읽을 수 있습니다.
또한 C ++은 객체 지향 언어이며, 포인터를 통해서만 달성 할 수있는 추상화 와 같은 OOP의 특정 측면이 있습니다. Java 및 C #과 같은 언어조차도 포인터를 광범위하게 사용하므로 포인터를 직접 조작하여 위험한 작업을 수행하지 못하게하지만 여전히 일단이 언어는 이해하기 시작합니다. 배후에서 모든 것이 포인터를 사용하여 수행된다는 것을 깨달았습니다.
따라서 포인터는 시간이 중요하고 메모리가 적은 응용 프로그램에서만 사용되는 것이 아니라 어디에서나 사용됩니다 .
struct foo f; init_foo(&f);
할당 f
한 다음 init_foo
해당 구조체에 대한 포인터로 호출 합니다. 매우 흔합니다. (그 포인터를 "위쪽으로"넘기지 않도록 조심하십시오.)
malloc
"버킷"에서 할당 된 블록을 클러스터링 할 때 매우 낮은 헤더 오버 헤드를 갖습니다. 반면에, 이것은 일반적으로 초과 할당으로 변환됩니다. 35 바이트를 요청하고 (알지 않고) 64를 얻으므로 29를 낭비합니다.
이것이 메모리 오버 헤드가 아닙니까?
물론, 추가 주소 (일반적으로 프로세서에 따라 4/8 바이트).
이것은 어떻게 보상됩니까?
그렇지 않습니다. 포인터에 필요한 간접 지시가 필요한 경우 지불해야합니다.
시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
나는 거기에서 많은 일을하지 않았지만 그렇게 생각합니다. 포인터 액세스는 어셈블리 프로그래밍의 기본 요소입니다. 사소한 양의 메모리가 필요하며 이러한 종류의 응용 프로그램의 상황에서도 포인터 작업이 빠릅니다.
나는 이것에 대해 Telastyn과 같은 스핀을 가지고 있지 않습니다.
임베디드 프로세서의 시스템 전역은 특정 하드 코딩 된 주소로 처리 될 수 있습니다.
프로그램의 전역은 전역 및 정적이 저장된 메모리의 위치를 가리키는 특수 포인터의 오프셋으로 처리됩니다.
지역 변수는 함수가 입력 될 때 나타나며 종종 "프레임 포인터"라고하는 다른 특수 포인터의 오프셋으로 처리됩니다. 여기에는 함수에 대한 인수가 포함됩니다. 스택 포인터로 푸시 및 팝에주의를 기울이면 프레임 포인터를 사용하지 않고 스택 포인터에서 바로 로컬 변수에 액세스 할 수 있습니다.
따라서 배열을 보거나 눈에 띄지 않는 로컬 또는 전역 변수를 가져 오는 경우 포인터의 간접 비용을 지불합니다. 변수의 종류에 따라 다른 포인터를 기반으로합니다. 잘 컴파일 된 코드는 포인터를 사용할 때마다 다시로드하지 않고 CPU 레지스터에 유지합니다.
C 및 C ++와 같은 언어에서 변수에 대한 포인터를 사용하는 동안 해당 주소를 저장하려면 하나 이상의 메모리 위치가 필요합니다. 이것이 메모리 오버 헤드가 아닙니까?
포인터를 저장해야한다고 가정합니다. 항상 그런 것은 아닙니다. 모든 변수는 일부 메모리 주소에 저장됩니다. 로 long
선언했다고 가정합니다 long n = 5L;
. 이것은 n
일부 주소에 대한 스토리지를 할당 합니다. 이 주소를 사용 *((char *) &n) = (char) 0xFF;
하여의 일부를 조작하는 것과 같은 멋진 일을 할 수 있습니다 n
. 의 주소 n
는 추가 오버 헤드로 어디에도 저장되지 않습니다.
이것은 어떻게 보상됩니까?
포인터가 명시 적으로 저장되어 있더라도 (예 : 목록과 같은 데이터 구조에) 결과 데이터 구조는 종종 포인터가없는 동등한 데이터 구조보다 더 우아합니다 (더 단순하고 이해하기 쉽고 처리하기 쉽습니다).
시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
예. 마이크로 컨트롤러를 사용하는 장치에는 메모리가 거의 없지만 펌웨어는 인터럽트 벡터 또는 버퍼 관리 등을 처리하기 위해 포인터를 사용할 수 있습니다.
gcc -fverbose-asm -S -O2
일부 C 코드 컴파일 시도 )
포인터가 있으면 약간의 오버 헤드가 발생하지만 거꾸로 볼 수도 있습니다. 포인터는 인덱스와 같습니다. C에서는 포인터로 인해 문자열 및 구조와 같은 복잡한 데이터 구조를 사용할 수 있습니다.
실제로 변수를 참조로 전달하고 전체 구조를 복제하고 변경 사항을 동기화하는 대신 포인터를 유지 관리하기가 쉽다고 가정합니다 (복사하더라도 포인터가 필요합니다). 포인터없이 비 연속 메모리 할당 및 할당 해제를 어떻게 처리합니까?
일반 변수조차도 변수가 가리키는 주소를 저장하는 기호 테이블에 항목이 있습니다. 따라서 메모리 (4 또는 8 바이트) 측면에서 많은 오버 헤드를 생성한다고 생각하지 않습니다. 자바와 같은 언어조차도 내부적으로 포인터를 사용하지만 (참조) JVM을 덜 안전하게 만들므로 조작 할 수 없습니다.
누락 된 데이터 유형과 같은 다른 선택이없는 경우에만 포인터를 사용해야합니다 (c에서) 포인터를 사용하면 올바르게 처리하지 않으면 오류가 발생할 수 있으므로 비교적 디버그하기가 어렵습니다.
이것이 메모리 오버 헤드가 아닙니까?
예 아니요 어쩌면?
머신의 메모리 주소 범위와 스택에 묶을 수없는 방식으로 메모리의 위치를 지속적으로 추적해야하는 소프트웨어를 상상하기 때문에 이는 까다로운 질문입니다.
예를 들어, 사용자가 다른 음악 파일을로드하려고 할 때 음악 파일이 사용자가 버튼을 눌러로드하고 휘발성 메모리에서 언로드하는 음악 플레이어를 상상해보십시오.
오디오 데이터가 저장된 위치를 어떻게 추적합니까? 메모리 주소가 필요합니다. 이 프로그램은뿐만 아니라 메모리뿐만 아니라 오디오 데이터 청크를 추적 할 필요 가 어디 메모리에. 따라서 메모리 주소 (예 : 포인터)를 유지해야합니다. 메모리 주소에 필요한 저장 공간의 크기는 머신의 주소 범위와 일치합니다 (예 : 64 비트 주소 지정 범위의 경우 64 비트 포인터).
따라서 "예"의 일종이며 메모리 주소를 추적하기 위해 스토리지가 필요하지만 동적으로 할당 된 메모리의 경우이를 피할 수있는 것은 아닙니다.
이것은 어떻게 보상됩니까?
포인터 자체의 크기에 대해서만 이야기하면 스택을 사용하여 비용을 피할 수 있습니다. 예를 들어 컴파일러는 포인터의 비용을 피하면서 상대 메모리 주소를 효과적으로 하드 코딩하는 명령어를 생성 할 수 있습니다. 그러나 이것은 가변 크기의 대규모 할당에 대해이 작업을 수행하면 스택 오버플로에 취약하고 사용자 입력에 의해 구동되는 복잡한 일련의 분기 (오디오 예제에서와 같이)가 불가능한 경우도 있습니다 (오직 불가능하지 않은 경우). 위).
또 다른 방법은 더 연속적인 데이터 구조를 사용하는 것입니다. 예를 들어, 노드 당 두 개의 포인터가 필요한 이중 연결 목록 대신 배열 기반 시퀀스를 사용할 수 있습니다. 또한이 두 가지의 하이브리드를 롤링되지 않은 목록처럼 사용할 수 있습니다.이 목록은 N 개의 모든 인접 요소 그룹 사이에 포인터 만 저장합니다.
시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
많은 성능이 중요한 응용 프로그램이 포인터 사용에 의해 지배되는 C 또는 C ++로 작성되므로 예, 매우 일반적으로 때문에, (그들은 스마트 포인터 나 같은 컨테이너 뒤에 수 있습니다 std::vector
또는 std::string
, 그러나 기본 메커니즘은 사용되는 포인터로 요약 동적 메모리 블록에 대한 주소를 추적합니다.
이제이 질문으로 돌아갑니다.
이것은 어떻게 보상됩니까? (두 번째 부분)
포인터는 일반적으로 백만 개 (64 비트 컴퓨터에서 측정 할 수있는 8 메가 바이트)처럼 저장하지 않는 한 더럽습니다.
Ben은 "거의"8 메가는 여전히 L3 캐시 크기라고 지적했다. 여기서 나는 총 DRAM 사용과 메모리 청크에 대한 일반적인 상대 크기의 관점에서 "정확하게"더 많은 포인터를 사용했다.
포인터가 비싼 곳은 포인터 자체가 아니라 다음과 같습니다.
동적 메모리 할당. 동적 메모리 할당은 기본 데이터 구조 (예 : 친구 또는 슬랩 할당 자)를 거쳐야하므로 비용이 많이 듭니다. 이것들은 종종 죽음에 최적화되어 있지만, 범용적이고 가변적 크기의 블록을 처리하도록 설계되었습니다.이 블록은 "검색"(가벼우면서도 일정한 시간 임에도 불구하고)과 비슷한 작업을 수행해야합니다. 메모리에서 연속 된 페이지의 무료 세트를 찾습니다.
메모리 액세스. 이것은 걱정하기에 더 큰 오버 헤드 인 경향이 있습니다. 처음으로 동적으로 할당 된 메모리에 액세스 할 때마다 강제 페이지 오류가 발생하고 캐시 누락으로 인해 메모리가 메모리 계층 아래로 이동하고 레지스터로 이동합니다.
메모리 액세스
메모리 액세스는 알고리즘 이외의 성능에서 가장 중요한 측면 중 하나입니다. AAA 게임 엔진과 같은 많은 성능 결정적인 분야는보다 효율적인 메모리 액세스 패턴 및 레이아웃으로 귀결되는 데이터 지향 최적화에 많은 에너지를 집중시킵니다.
가비지 수집기를 통해 각 사용자 정의 유형을 별도로 할당하려는 고급 언어의 가장 큰 성능 문제 중 하나는 메모리를 상당히 조각화 할 수 있다는 것입니다. 모든 객체가 한 번에 할당되지 않는 경우 특히 그렇습니다.
이 경우 사용자 정의 객체 유형의 백만 인스턴스 목록을 저장하는 경우 루프에서 순차적으로 해당 인스턴스에 액세스하면 메모리 영역이 서로 다른 백만 포인터 목록과 유사하므로 상당히 느려질 수 있습니다. 이 경우 아키텍처는 제거하기 전에 해당 청크의 주변 데이터에 액세스 할 수 있도록 정렬 된 큰 청크에서 더 높고 느리고 더 큰 레벨의 계층 구조에서 메모리를 페치하려고합니다. 이러한 목록의 각 객체가 개별적으로 할당되면 종종 각 후속 반복이 제거 전에 인접한 객체가 액세스되지 않고 메모리의 완전히 다른 영역에서로드되어야 할 때 캐시 누락으로 인해 비용을 지불하게됩니다.
이러한 언어에 대한 많은 컴파일러가 요즘 명령어 선택 및 레지스터 할당에서 실제로 훌륭한 일을하고 있지만 여기에서 메모리 관리에 대한 직접적인 제어가 부족하면 킬러가 될 수 있지만 (오류가 발생하기 쉽지는 않지만) 여전히 언어를 만들 수 있습니다 C와 C ++은 꽤 인기가 있습니다.
간접적으로 포인터 액세스 최적화
성능이 가장 중요한 시나리오에서 응용 프로그램은 종종 참조 청산 성을 향상시키기 위해 인접한 청크에서 메모리를 풀링하는 메모리 풀을 사용합니다. 그러한 경우, 노드의 메모리 레이아웃이 본질적으로 연속적이라면 트리 또는 링크 된리스트와 같은 링크 된 구조조차 캐시 친화적으로 만들 수 있습니다. 이는 간접적으로 참조를 역 참조 할 때 관련된 참조의 지역성을 개선함으로써 포인터 역 참조를보다 저렴하게 효과적으로 만들고 있습니다.
쫓는 포인터
다음과 같이 단일 연결 목록이 있다고 가정하십시오.
Foo->Bar->Baz->null
문제는 이러한 모든 노드를 범용 할당 자에 대해 별도로 할당하면 (한 번에 모두는 아님) 실제 메모리가 다음과 같이 다소 분산 될 수 있다는 것입니다 (단순 다이어그램).
포인터를 쫓기 시작하고 Foo
노드에 액세스하면 다음 과 같이 메모리 영역에서 더 느린 메모리 영역에서 더 빠른 메모리 영역으로 청크를 이동하는 강제 누락 (및 페이지 오류)으로 시작합니다.
이로 인해 메모리 영역을 캐시하여 페이지의 일부에만 액세스하고이 목록 주위에서 포인터를 추적 할 때 나머지를 제거합니다. 그러나 메모리 할당자를 제어함으로써 다음과 같은 목록을 연속적으로 할당 할 수 있습니다.
... 따라서 이러한 포인터를 역 참조하고 포인트를 처리 할 수있는 속도를 크게 향상시킵니다. 따라서 매우 간접적이지만이 방법으로 포인터 액세스 속도를 높일 수 있습니다. 물론 배열에 연속으로 저장하면 처음에는이 문제가 발생하지 않지만 여기에서 메모리 레이아웃을 명시 적으로 제어 할 수있는 메모리 할당자는 연결된 구조가 필요한 날을 절약 할 수 있습니다.
* 참고 : 이것은 메모리 계층 구조와 참조 위치에 대한 매우 단순화 된 다이어그램이며 토론이지만 질문의 수준에 적합하기를 바랍니다.
이것이 메모리 오버 헤드가 아닙니까?
실제로 메모리 오버 헤드이지만 매우 작습니다 (무의미한 점까지).
이것은 어떻게 보상됩니까?
보상되지 않습니다. 포인터를 통한 데이터 액세스 (포인터 참조)가 매우 빠르다는 것을 알아야합니다 (정확하게 기억한다면 역 참조 당 하나의 어셈블리 명령을 사용합니다). 많은 경우에 가장 빠른 대안이 될만큼 빠릅니다.
시간이 중요한 메모리 부족 애플리케이션에 포인터가 사용됩니까?
예.
해당 포인터가 필요한 동안 추가 메모리 사용량 (일반적으로 포인터 당 4-8 바이트) 만 필요합니다. 이것을 더 저렴하게 만드는 많은 기술이 있습니다.
포인터를 강력하게 만드는 가장 기본적인 기술은 모든 포인터를 유지할 필요가 없다는 것입니다. 때로는 알고리즘을 사용하여 포인터에서 다른 것으로 포인터를 구성 할 수 있습니다. 가장 간단한 예는 배열 산술입니다. 50 개의 정수 배열을 할당하면 각 정수마다 하나씩 50 개의 포인터를 유지할 필요가 없습니다. 일반적으로 한 포인터 (첫 번째 포인터)를 추적하고 포인터 산술을 사용하여 다른 포인터를 즉석에서 생성합니다. 때로는 배열의 특정 요소에 대한 포인터 중 하나를 필요할 때만 일시적으로 유지할 수 있습니다. 완료 한 후에는 필요할 경우 나중에 다시 생성하기에 충분한 정보를 유지 한 경우 폐기 할 수 있습니다. 이것은 사소한 것처럼 들릴지 모르지만 정확하게 보존 도구의 종류입니다.
매우 타이트한 메모리 상황에서는 비용을 최소화하는 데 사용할 수 있습니다. 매우 좁은 메모리 공간 에서 작업하는 경우 일반적으로 얼마나 많은 객체를 조작해야하는지 잘 알고 있습니다. 한 번에 하나씩 많은 정수를 할당하고 그에 대한 전체 포인터를 유지하는 대신이 특정 알고리즘에서 256 개 이상의 정수를 가질 수 없다는 개발자 지식을 활용할 수 있습니다. 이 경우 첫 번째 정수에 대한 포인터를 유지하고 전체 포인터 (4/8 바이트)를 사용하는 대신 문자 (1 바이트)를 사용하여 색인을 추적 할 수 있습니다. 알고리즘 트릭을 사용하여 이러한 인덱스 중 일부를 즉석에서 생성 할 수도 있습니다.
이런 종류의 메모리 양심은 과거에 매우 인기가있었습니다. 예를 들어, NES 게임은 데이터를 모두 저장하지 않고 데이터를 저장하고 알고리즘으로 포인터를 생성하는 능력에 광범위하게 의존합니다.
극단적 인 메모리 상황은 컴파일 타임에 작업하는 모든 공간을 할당하는 것과 같은 일을하도록 할 수도 있습니다. 그런 다음 해당 메모리에 저장해야 할 포인터는 데이터가 아닌 프로그램에 저장됩니다. 많은 메모리 제한 상황에서 별도의 프로그램 및 데이터 메모리 (ROM과 RAM)가 있으므로 알고리즘을 사용하여 포인터를 해당 프로그램 메모리로 푸시하는 방법을 조정할 수 있습니다.
기본적으로 모든 오버 헤드를 제거 할 수는 없습니다. 그러나 제어 할 수 있습니다. 알고리즘 기법을 사용하여 저장할 수있는 포인터 수를 최소화 할 수 있습니다. 동적 메모리에 대한 포인터를 사용하는 경우 해당 동적 메모리 스폿에 대한 1 포인터를 유지하는 비용보다 적은 비용을 들이지 않습니다. 이는 해당 메모리 블록의 모든 항목에 액세스하는 데 필요한 최소한의 정보이기 때문입니다. 그러나 매우 엄격한 메모리 제약 조건 시나리오에서는 이것이 특별한 경우 인 경향이 있습니다 (동적 메모리 및 매우 엄격한 메모리 제약 조건은 같은 상황에 나타나지 않는 경향이 있습니다).