포인터 산술을 통한 어레이 값 액세스 대 C의 서브스크립션
저는 C에서 포인터 산술을 사용하는 것이 일반적으로 배열 액세스를 위한 구독보다 빠르다는 것을 계속 읽고 있습니다.최신(최적화된 것으로 추정되는) 컴파일러에서도 이것이 사실입니까?
그렇다면, 제가 C를 배우는 것에서 벗어나 Mac에서 Objective-C와 코코아로 이동하기 시작할 때에도 여전히 그러합니까?
C와 Objective-C 모두에서 어레이 액세스에 선호되는 코딩 스타일은 무엇입니까?(각 언어의 전문가들에 의해) 더 읽기 쉽고, 더 "정확한"(더 나은 용어가 없기 때문에) 것으로 간주되는 것은 무엇입니까?
당신은 이 주장의 배후에 있는 이유를 이해해야 합니다.당신은 왜 그것이 더 빠른지 스스로에게 의문을 제기해 본 적이 있습니까?몇 가지 코드를 비교해 보겠습니다.
int i;
int a[20];
// Init all values to zero
memset(a, 0, sizeof(a));
for (i = 0; i < 20; i++) {
printf("Value of %d is %d\n", i, a[i]);
}
그들은 모두 0입니다, 정말 놀랍습니다:-P 질문은, 무엇을 의미하는지입니다.a[i]
실제로 낮은 수준의 기계 코드로?라는 뜻입니다.
는 를사다니용합소의주다▁of▁the▁take▁address니.
a
더하다
i
항크 기의 ▁of의 단일 항목 의 곱a
해당 주소(일반적으로 4바이트)로 이동합니다.해당 주소에서 값을 가져옵니다.
이 그서매번값에서 마다.a
의 기본 a
는 의곱 의 곱셈 에 추가됩니다.i
만 수행할 수 있습니다.포인터의 참조를 취소하는 경우 1단계와 2단계를 수행할 필요가 없으며 3단계만 수행할 수 있습니다.
아래 코드를 고려해 보십시오.
int i;
int a[20];
int * b;
memset(a, 0, sizeof(a));
b = a;
for (i = 0; i < 20; i++) {
printf("Value of %d is %d\n", i, *b);
b++;
}
이 코드가 더 빠를 수도 있습니다...하지만 그렇다고 해도, 그 차이는 작습니다.왜 더 빠를까요?"*b"는 위의 3단계와 동일합니다.그러나 "b++"는 1단계 및 2단계와 동일하지 않습니다. "b++"는 포인터를 4만큼 증가시킵니다.
(신입생에게 중요: 달리기
++
포인터에서는 메모리에서 포인터가 1바이트 증가하지 않습니다!포인터가 가리키는 데이터의 크기만큼 메모리의 바이트 수가 증가합니다.그것은 다음을 가리킵니다.int
리고그고.int
내 기계에서 4바이트이므로, b++는 b를 4만큼 증가시킵니다!)
좋아요, 그런데 왜 그게 더 빠를까요?하는 것이 포터를 4를 곱하는 입니다.i
4를 더하면 포인터에 추가할 수 있습니다.두 경우 모두 추가되지만 두 번째 경우에는 곱셈이 없습니다(하나의 곱셈에 필요한 CPU 시간을 피할 수 있습니다).현대 CPU의 속도를 고려할 때, 어레이가 1 mio 요소였다고 해도, 정말로 차이를 벤치마킹할 수 있는지 궁금합니다.
현대의 컴파일러가 둘 중 하나를 동등하게 빠르게 최적화할 수 있다는 것은 그것이 생산하는 어셈블리 출력을 보고 확인할 수 있는 것입니다.이렇게 하려면 "-S" 옵션(자본 S)을 GCC에 전달합니다.
첫 번째 C 코드의 코드입니다(최적화 수준).-Os
속도에 눈에 띄게 늘릴 -O2
는과전다른과 많이 .-O3
):
_main:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $108, %esp
call ___i686.get_pc_thunk.bx
"L00000000001$pb":
leal -104(%ebp), %eax
movl $80, 8(%esp)
movl $0, 4(%esp)
movl %eax, (%esp)
call L_memset$stub
xorl %esi, %esi
leal LC0-"L00000000001$pb"(%ebx), %edi
L2:
movl -104(%ebp,%esi,4), %eax
movl %eax, 8(%esp)
movl %esi, 4(%esp)
movl %edi, (%esp)
call L_printf$stub
addl $1, %esi
cmpl $20, %esi
jne L2
addl $108, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
두 번째 코드도 마찬가지입니다.
_main:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $124, %esp
call ___i686.get_pc_thunk.bx
"L00000000001$pb":
leal -104(%ebp), %eax
movl %eax, -108(%ebp)
movl $80, 8(%esp)
movl $0, 4(%esp)
movl %eax, (%esp)
call L_memset$stub
xorl %esi, %esi
leal LC0-"L00000000001$pb"(%ebx), %edi
L2:
movl -108(%ebp), %edx
movl (%edx,%esi,4), %eax
movl %eax, 8(%esp)
movl %esi, 4(%esp)
movl %edi, (%esp)
call L_printf$stub
addl $1, %esi
cmpl $20, %esi
jne L2
addl $124, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
글쎄요, 그건 달라요, 그건 확실해요.와 108의 변수에서 .b
(첫 번째 코드에서는 스택에 하나의 변수가 줄었지만, 이제 스택 주소를 변경하는 하나의 변수가 더 있습니다.의 실제 for
는 다음과 같습니다.
movl -104(%ebp,%esi,4), %eax
와 비교하여
movl -108(%ebp), %edx
movl (%edx,%esi,4), %eax
사실 첫 번째 접근 방식은 두 개의 기계 코드가 있는 대신 모든 작업을 수행하기 위해 하나의 CPU 기계 코드를 발행하기 때문에(!) 더 빨라 보입니다.반면에 아래의 두 어셈블리 명령은 위의 명령보다 런타임이 더 낮을 수 있습니다.
마지막으로 컴파일러와 CPU 기능(CPU가 어떤 방식으로 메모리에 액세스할 수 있도록 제공하는 명령)에 따라 결과가 어느 쪽이든 달라질 수 있습니다.둘 중 하나가 더 빠를 수도 있고 더 느릴 수도 있습니다.의 컴파일러버전을 와특정 한 수 . 의 어셈블리 더 할 수 있기 때문에( 했기 에) 하나로컴즉러일(파, 하와버전)의하없수다니습확한할신않의는나자의지특제하정한정게하확신을나▁multiply▁you즉없▁for다수하,▁really니습로나▁say▁as와▁unless▁sure확▁(▁exactly▁address▁yourself▁command▁the할▁(▁you의▁limit신▁cpu▁cannot한 CPU가 하나의 어셈블리 명령으로 점점 더 많은 작업을 수행할 수 있기 때문입니다(몇 년 전에는 컴파일러가 실제로 수동으로 주소를 가져와 곱해야 했습니다).i
값을 가져오기 전에 4를 더하고 둘 다를 더함), 옛날에는 절대적인 진리였던 진술이 오늘날 점점 더 의심스러워지고 있습니다.또한 CPU가 내부적으로 어떻게 작동하는지 아는 사람이 있습니까?위에서 한 조립 지침을 다른 두 조립 지침과 비교합니다.
나는 명령어 수가 다르고 필요한 시간도 다를 수 있다는 것을 알 수 있습니다.또한 이러한 명령어가 시스템 프레젠테이션에 필요한 메모리 양(메모리에서 CPU 캐시로 전송해야 함)도 다릅니다.그러나 최신 CPU는 사용자가 공급하는 방식으로 명령을 실행하지 않습니다.이들은 큰 명령어(CISC라고도 함)를 작은 하위 명령어(RISC라고도 함)로 분할하여 내부적으로 프로그램 흐름을 보다 효율적으로 최적화할 수 있도록 합니다.실제로 첫 번째 단일 명령과 아래의 다른 두 명령은 동일한 하위 명령 집합을 생성할 수 있으며, 이 경우 측정 가능한 속도 차이가 전혀 없습니다.
Objective-C와 관련하여 확장자가 있는 C입니다.따라서 C에 적용되는 모든 것은 포인터와 배열 측면에서 목표-C에도 적용됩니다. 개체사는예경우하용를예경:(우▁ifforNSArray
또는NSMutableArray
입니다.), 이은완다른짐승다니입전히것▁),다니.그러나 이 경우에는 메소드를 사용하여 이러한 어레이에 액세스해야 하므로 선택할 수 있는 포인터/어레이 액세스 권한이 없습니다.
"일반적으로 포인터 산술을 사용하는 것이 배열 액세스에 대한 구독보다 빠름"
아니요, 어느 쪽이든 같은 수술입니다.첨자는 배열의 시작 주소에 요소 크기 * 인덱스를 추가하기 위한 구문 설탕입니다.
즉, 배열의 요소를 반복할 때 첫 번째 요소에 포인터를 가져 루프를 통과할 때마다 증가하는 것이 루프 변수에서 현재 요소의 위치를 계산하는 것보다 일반적으로 약간 더 빠릅니다.(실시간 애플리케이션에서 이 문제가 크게 중요한 것은 드문 일이지만,먼저 알고리즘을 검토하십시오. 조기 최적화가 모든 악의 근원입니다. 등)
실행 속도와 관련된 질문에 답하지 않기 때문에 약간 주제에서 벗어난 내용일 수 있지만(죄송합니다), 조기 최적화가 모든 악의 근원이라는 점을 고려해야 합니다(Knuth).제 생각에는, 특히 그 언어를 아직 (다시) 배울 때는, 어떻게든 먼저 읽기 쉬운 방법으로 그것을 쓰십시오.그런 다음 프로그램이 올바르게 실행되면 속도를 최적화하는 것이 좋습니다.대부분의 경우 코드를 작성하는 속도가 충분히 빠릅니다.
Mecki는 훌륭한 설명을 합니다.제 경험으로 볼 때, 인덱싱 대 포인터에서 종종 중요한 것 중 하나는 다른 코드가 루프에 있는 것입니다.예:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>
using namespace std;
typedef int64_t int64;
static int64 nsTime() {
struct timespec tp;
clock_gettime(CLOCK_REALTIME, &tp);
return tp.tv_sec*(int64)1000000000 + tp.tv_nsec;
}
typedef int T;
size_t const N = 1024*1024*128;
T data[N];
int main(int, char**) {
cout << "starting\n";
{
int64 const a = nsTime();
int sum = 0;
for (size_t i=0; i<N; i++) {
sum += data[i];
}
int64 const b = nsTime();
cout << "Simple loop (indexed): " << (b-a)/1e9 << "\n";
}
{
int64 const a = nsTime();
int sum = 0;
T *d = data;
for (size_t i=0; i<N; i++) {
sum += *d++;
}
int64 const b = nsTime();
cout << "Simple loop (pointer): " << (b-a)/1e9 << "\n";
}
{
int64 const a = nsTime();
int sum = 0;
for (size_t i=0; i<N; i++) {
int a = sum+3;
int b = 4-sum;
int c = sum+5;
sum += data[i] + a - b + c;
}
int64 const b = nsTime();
cout << "Loop that uses more ALUs (indexed): " << (b-a)/1e9 << "\n";
}
{
int64 const a = nsTime();
int sum = 0;
T *d = data;
for (size_t i=0; i<N; i++) {
int a = sum+3;
int b = 4-sum;
int c = sum+5;
sum += *d++ + a - b + c;
}
int64 const b = nsTime();
cout << "Loop that uses more ALUs (pointer): " << (b-a)/1e9 << "\n";
}
}
빠른 Core 2 기반 시스템(g++ 4.1.2, x64)의 경우 타이밍은 다음과 같습니다.
단순 루프(색인화): 0.400842단순 루프(포인터): 0.380633더 많은 ALU를 사용하는 루프(인덱스): 0.768398더 많은 ALU(포인터)를 사용하는 루프: 0.777886
인덱싱이 더 빠를 수도 있고 포인터 산술이 더 빠를 수도 있습니다.이는 CPU와 컴파일러가 루프 실행을 파이프라인으로 처리할 수 있는 방법에 따라 달라집니다.
배열 형식의 데이터를 다루는 경우에는 첨자를 사용하면 코드를 더 쉽게 읽을 수 있습니다.오늘날의 기계(특히 이와 같은 간단한 것의 경우)에서는 읽을 수 있는 코드가 더 중요합니다.
만약 당신이 malloc()한 데이터 덩어리를 명시적으로 다루고 있고 그 데이터 안에 포인터를 가지고 싶다면, 예를 들어 오디오 파일 헤더 안에 20바이트를 넣는다면, 주소 산술이 당신이 하려는 것을 더 명확하게 표현한다고 생각합니다.
이와 관련하여 컴파일러 최적화에 대해 확신할 수 없지만, 구독이 느리더라도 기껏해야 몇 시간의 클럭 주기만큼 느릴 뿐입니다.여러분이 생각의 명확성으로부터 훨씬 더 많은 것을 얻을 수 있다면 그것은 거의 아무것도 아닙니다.
편집: 이러한 다른 응답 중 일부에 따르면, 구독은 구문론적 요소일 뿐이며 제가 생각한 것처럼 성능에 영향을 미치지 않습니다.이 경우 포인터가 가리키는 블록 내의 액세스 데이터를 통해 표현하려는 컨텍스트와 일치해야 합니다.
슈퍼스칼라 CPU 등이 있는 기계 코드를 봐도 실행 속도를 예측하기 어렵다는 점을 명심하시기 바랍니다.
- 비정상적인 검사
- 파이프라이닝
- 분기 예측
- 하이퍼스레딩
- ...
기계 명령만 세고 시계 원기둥만 세는 것이 아닙니다.정말로 필요한 경우에 측정하는 것이 더 쉬워 보입니다.주어진 프로그램에 대한 정확한 사이클 수를 계산하는 것이 불가능하지는 않더라도(우리는 대학에서 그것을 해야만 했습니다), 그것은 거의 재미가 없고 맞추기가 어렵습니다.참고 사항:멀티 스레드/멀티 프로세서 환경에서도 정확한 측정이 어렵습니다.
char p1[ ] = "12345";
char* p2 = "12345";
char *ch = p1[ 3 ]; /* 4 */
ch = *(p2 + 3); /* 4 */
C 표준은 어느 것이 더 빠르다고 말하지 않습니다.관찰 가능한 동작은 동일하며 원하는 방식으로 구현하는 것은 컴파일러에 달려 있습니다.기억을 전혀 읽지 않는 경우가 많습니다.
일반적으로 컴파일러, 버전, 아키텍처 및 컴파일 옵션을 지정하지 않으면 어떤 것이 "더 빠릅니까?"라고 말할 수 없습니다.그럼에도 최적화는 주변 환경에 따라 달라집니다.
따라서 일반적인 조언은 더 명확하고 간단한 코드를 사용하라는 것입니다.array[i]를 사용하면 인덱스 아웃오브바운드 조건을 검색할 수 있는 몇 가지 도구 기능을 제공하므로 array를 사용하는 경우 이러한 조건을 그대로 처리하는 것이 좋습니다.
중요한 경우 컴파일러가 생성하는 어셈블리어를 조사합니다.그러나 주변 코드를 변경하면 변경될 수 있습니다.
아니요, 포인터 산술을 사용하는 것이 더 빠르지 않고 더 느릴 수도 있습니다. 최적화 컴파일러는 추가 또는 추가/물보다 빠른 포인터 산술을 위해 Intel 프로세서의 LEA(Load Effective Address) 또는 다른 프로세서의 유사한 명령을 사용할 수 있기 때문입니다.여러 가지 작업을 동시에 수행할 수 있고 플래그에 영향을 미치지 않는다는 장점이 있으며, 계산하는 데도 한 사이클이 소요됩니다.참고로, 아래는 GCC 매뉴얼에서 나온 것입니다. 그래서.-Os
주로 속도를 최적화하지 않습니다.
저도 그 말에 전적으로 동의합니다.먼저 깨끗하고 읽기 쉽고 재사용 가능한 코드를 작성한 다음 최적화에 대해 생각하고 몇 가지 프로파일링 도구를 사용하여 병목 현상을 찾습니다.대부분의 경우 성능 문제는 I/O와 관련되거나 잘못된 알고리즘 또는 버그로 인해 검색해야 합니다.크누스가 그 남자야 ;-)
구조 배열로 무엇을 할 것인가 하는 생각이 들었습니다.포인터 연산을 수행하려면 반드시 구조체의 각 멤버에 대해 수행해야 합니다.오버킬처럼 들리시나요?네, 물론 그것은 과잉 살상이고 또한 그것은 모호한 벌레들에게 넓은 문을 열어줍니다.
-Os
크기에 맞게 최적화합니다.Os
일반적으로 코드 크기를 늘리지 않는 모든 O2 최적화를 활성화합니다.또한 코드 크기를 줄이기 위해 설계된 추가 최적화를 수행합니다.
그것은 실이 아니에요.첨자 연산자만큼 정확하게 빠릅니다.Objective-C에서는 동적 호출 특성으로 인해 모든 호출에서 일부 작업을 수행하기 때문에 객체 지향 스타일이 훨씬 느린 C 및 객체 지향 스타일과 같은 배열을 사용할 수 있습니다.
속도에 차이가 있을 것 같지는 않습니다.
C++에서처럼 다른 컨테이너(예: 벡터)와 동일한 구문을 사용할 수 있으므로 배열 연산자 []를 사용하는 것이 좋습니다.
저는 10년 동안 여러 AAA 타이틀에 대한 C++/어셈블리 최적화 작업을 해 왔으며, 제가 작업한 특정 플랫폼/컴파일러에서 포인터 산술은 상당히 큰 차이를 보였다고 말할 수 있습니다.
예를 들어, 모든 어레이 액세스를 포인터 산술로 대체하여 동료들의 완전한 불신을 해소함으로써 파티클 생성기의 루프를 40% 더 빠르게 만들 수 있었습니다.예전에는 선생님께 좋은 방법으로 들었습니다만, 현재 보유하고 있는 컴파일러/CPU와 차이가 없을 것이라고 생각했습니다.제가 틀렸어요 ;)
많은 콘솔 ARM 프로세서가 현대 CISC CPU의 귀여운 기능을 모두 갖추고 있지 않으며 컴파일러가 때때로 약간 흔들렸다는 점을 지적해야 합니다.
언급URL : https://stackoverflow.com/questions/233148/accessing-array-values-via-pointer-arithmetic-vs-subscripting-in-c
'programing' 카테고리의 다른 글
M1 Mac의 'root'@'localhost' 사용자에 대해 'mysql_upgrade' 액세스가 거부되었습니다. (0) | 2023.07.25 |
---|---|
\n이 있는 파이썬 스트립 (0) | 2023.07.25 |
명령줄에서 C 프로그램으로 인수 전달 (0) | 2023.07.25 |
인덱스/고유 필드에서 쿼리할 때 MySQL "LIMIT 1"을 사용하는 포인트가 있습니까? (0) | 2023.07.25 |
각도 2: 양식이 연결되지 않아 양식 제출이 취소되었습니다. (0) | 2023.07.25 |