포인터를 전달하는 대신 C에서 값으로 구조체를 전달하는 데 단점이 있습니까?


157

포인터를 전달하는 대신 C에서 값으로 구조체를 전달하는 데 단점이 있습니까?

구조체가 큰 경우 많은 양의 데이터를 복사하는 성능 측면이 분명히 있지만 작은 구조체의 경우 기본적으로 여러 값을 함수에 전달하는 것과 동일해야합니다.

반환 값으로 사용하면 더 재미있을 것입니다. C에는 함수의 단일 반환 값만 있지만 종종 여러 값이 필요합니다. 따라서 간단한 해결책은 구조체에 넣고 반환하는 것입니다.

이에 대한 이유가 있습니까?

내가 여기서 말하는 것에 대해 모든 사람에게 분명하지 않을 수 있으므로 간단한 예를 들어 보겠습니다.

C로 프로그래밍하는 경우 조만간 다음과 같은 함수 작성이 시작됩니다.

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

이것은 문제가되지 않습니다. 유일한 문제는 매개 변수의 순서에 따라 동료와 동의해야 모든 기능에서 동일한 규칙을 사용할 수 있다는 것입니다.

그러나 같은 종류의 정보를 반환하려는 경우 어떻게됩니까? 일반적으로 다음과 같은 것을 얻습니다.

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

이것은 잘 작동하지만 훨씬 더 문제가 있습니다. 반환 값은이 구현에서는 그렇지 않은 것을 제외하고는 반환 값입니다. 위에서 get_data 함수가 len이 가리키는 것을 볼 수 없다는 것을 알 수있는 방법이 없습니다. 그리고 컴파일러가 실제로 그 포인터를 통해 값이 반환되는지 확인하는 것은 없습니다. 따라서 다음 달 누군가가 코드를 제대로 이해하지 않고 코드를 수정하면 (문서를 읽지 않았기 때문에) 다른 사람이 모르게 깨지거나 무작위로 충돌하기 시작합니다.

그래서 내가 제안하는 해결책은 간단한 구조체입니다.

struct blob { char *ptr; size_t len; }

예제는 다음과 같이 다시 작성할 수 있습니다.

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

어떤 이유로, 나는 대부분의 사람들이 본능적으로 exam_data가 struct blob에 대한 포인터를 갖도록 만들 것이라고 생각하지만 그 이유는 모르겠습니다. 여전히 포인터와 정수를 얻습니다. 그리고 get_data 경우 길이에 대한 입력 값이 없으며 반환 된 길이가 있어야하기 때문에 앞에서 설명한 방식으로 엉망이 될 수 없습니다.


가치가있는 void examine data(const struct blob)것은 틀립니다.
Chris Lutz

감사합니다. 변수 이름을 포함하도록 변경했습니다.
dkagedal

1
"위에서 get_data 함수가 len이 가리키는 것을 볼 수 없다는 것을 알 수있는 방법이 없습니다. 그리고 컴파일러가 그 포인터를 통해 값이 실제로 리턴되는지 확인하는 것은 없습니다." -이것은 전혀 이해가되지 않습니다 (아마도 함수 외부에 나타나는 마지막 두 줄로 인해 예제가 유효하지 않은 코드 일 수 있습니다). 좀 더 자세히 설명해 주시겠습니까?
Adam Spiers

2
함수 아래의 두 줄은 함수 호출 방법을 보여줍니다. 함수 서명은 구현이 포인터에만 쓸 것이라는 사실을 암시하지 않습니다. 그리고 컴파일러는 포인터에 값이 기록되어 있는지 확인해야한다는 것을 알 수 없으므로 반환 값 메커니즘은 문서에서만 설명 할 수 있습니다.
dkagedal

1
사람들이 C에서 이것을 더 자주하지 않는 주된 이유는 역사적입니다. C89 이전에는 값으로 구조체를 전달하거나 반환 할 수 없었 으므로 C89 이전의 논리적 시스템 gettimeofday은 포인터를 대신 사용 하여 논리적으로 수행 해야하는 모든 시스템 인터페이스를 예로 들었습니다.
zwol

답변:


202

작은 구조체 (예 : 점, rect)의 경우 값으로 전달하는 것이 완벽하게 허용됩니다. 그러나 속도와는 별도로 큰 구조체를 값으로 신중하게 전달 / 반환해야하는 또 다른 이유는 스택 공간입니다.

많은 C 프로그래밍은 임베디드 시스템을위한 것이며, 메모리가 부족하고 스택 크기는 KB 또는 바이트로 측정 될 수 있습니다. 값으로 구조체를 전달하거나 반환하면 해당 구조체의 복사본이 배치됩니다. 스택은 잠재적 으로이 사이트의 이름을 딴 상황을 야기합니다 .

과도한 스택 사용을 가진 것으로 보이는 응용 프로그램을 보면 값으로 전달되는 구조체가 내가 가장 먼저 찾는 것 중 하나입니다.


2
"값으로 구조체를 전달하거나 반환하면 해당 구조체의 복사본이 스택에 배치됩니다." 그렇게하는 모든 툴체인을 braindead 라고 부릅니다 . 그렇습니다. 많은 사람들이 그렇게 할 것이 슬프지만 C 표준이 요구하는 것은 아닙니다. 제정신 컴파일러는 모든 것을 최적화합니다.
Reinstate Monica

1
@KubaOber 이것이 자주 끝나지 않는 이유입니다 : stackoverflow.com/questions/552134/…
Roddy

1
작은 구조체와 큰 구조체를 구분하는 명확한 선이 있습니까?
Josie Thompson

63

언급되지 않은 이것을하지 않는 한 가지 이유는 바이너리 호환성이 중요한 문제를 야기 할 수 있기 때문입니다.

사용 된 컴파일러에 따라 구조는 컴파일러 옵션 / 구현에 따라 스택 또는 레지스터를 통해 전달 될 수 있습니다.

참조 : http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

Freg-struct-return

두 컴파일러가 동의하지 않으면 문제가 발생할 수 있습니다. 이 작업을 수행하지 않는 주요 이유는 스택 소비 및 성능 이유입니다.


4
이것은 내가 찾고있는 일종의 대답이었습니다.
dkagedal

2
사실이지만 이러한 옵션은 값별 패스와 관련이 없습니다. 그들은 완전히 다른 구조체 를 반환 하는 것과 관련이 있습니다. 참조로 물건을 반납하는 것은 대개 양발로 자신을 촬영하는 확실한 방법입니다. int &bar() { int f; int &j(f); return j;};
Roddy

19

이 질문에 실제로 답하기 위해서는 집회 땅을 깊이 파고 들어야합니다.

(다음 예제는 x86_64에서 gcc를 사용합니다. 누구나 MSVC, ARM 등과 같은 다른 아키텍처를 추가 할 수 있습니다.)

예제 프로그램을 보자.

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

완전 최적화로 컴파일

gcc -Wall -O3 foo.c -o foo

어셈블리를보십시오 :

objdump -d foo | vim -

이것이 우리가 얻는 것입니다.

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

nopl패드를 제외하면 give_two_doubles()27 바이트, give_point()29 바이트가 있습니다. 반면에, give_point()하나보다 적은 명령을 내린다give_two_doubles()

흥미로운 점은 컴파일러가 mov더 빠른 SSE2 변형 movapd및 로 최적화 할 수 있다는 점 movsd입니다. 또한 give_two_doubles()실제로 메모리에서 데이터를주고 받음으로써 속도가 느려집니다.

분명히이 중 많은 부분이 임베디드 환경 (C의 경기장이 현재 대부분의 시간)에 적용되지 않을 수 있습니다. 나는 어셈블리 마법사가 아니므로 의견을 환영합니다!


6
큰 차이를 보여 주거나 예측하기 어려운 점프의 숫자와 같은 더 흥미로운 측면을 계산할 수 없다면 명령의 수를 계산하는 것이 그다지 흥미롭지는 않습니다. 실제 성능 속성은 명령 수보다 훨씬 미묘합니다. .
dkagedal

6
@dkagedal : 맞습니다. 돌이켜 보면 내 대답은 매우 잘못 작성된 것 같습니다. 비록 많은 명령어 수에 초점을 맞추지 않았지만 (P는 인상을주지 못했습니다.) 실제로 중요한 것은 구조체를 값으로 전달하는 것이 작은 유형의 참조로 전달하는 것보다 바람직하다는 것입니다. 어쨌든, 값으로 전달하는 것이 더 간단하기 때문에 (평생 저글링, 누군가 데이터를 변경하거나 const항상 걱정할 필요가 없음) 값별 복사에서 성능이 저하되지 않는다는 것을 알았습니다. 많은 사람들이 믿는 것과는 반대로.
kizzx2

15

간단한 해결책은 오류 코드를 반환 값으로 반환하고 그 밖의 모든 것을 함수의 매개 변수로 반환합니다.
이 매개 변수는 구조체 일 수는 있지만 값으로 전달하는 특별한 이점을 보지 못하고 포인터를 보냈습니다.
값으로 구조를 전달하는 것은 위험합니다. 전달하는 것이 매우 신중해야합니다 .C에 복사 생성자가 없다는 것을 기억하십시오. 구조 매개 변수 중 하나가 포인터 인 경우 포인터 값이 복사되면 매우 혼란스럽고 어려울 수 있습니다 유지하십시오.

단지에 대한 답 (전체 신용 완료 로디을 ) 스택 사용은 값으로 구조를 전달하지 않는 또 다른 이유입니다. 스택 오버플로 디버깅이 실제 PITA라고 생각합니다.

댓글 재생 :

포인터로 구조체를 전달하면 일부 엔티티 가이 객체에 대한 소유권을 가지고 있으며 언제 그리고 언제 릴리스되어야 하는지를 완전히 알고 있습니다. 값으로 구조체를 전달하면 구조체의 내부 데이터에 대한 숨겨진 참조 (다른 구조 등을 가리키는 포인터 등)가 유지 관리하기가 어렵습니다 (가능한 이유는 무엇입니까?).


6
그러나 포인터를 전달하는 것이 구조체에 넣었 기 때문에 더 위험하지는 않습니다. 그래서 나는 그것을 사지 않습니다.
dkagedal

포인터가 포함 된 구조를 복사하는 데 도움이됩니다. 이 점은 분명하지 않을 수 있습니다. 자신이 무엇을 말하는지 모르는 사람들은 딥 카피와 얕은 카피를 검색하십시오.
zooropa

1
C 함수 규칙 중 하나는 입력 매개 변수보다 먼저 출력 매개 변수를 나열하는 것입니다. 예 : int func (char * out, char * in);
zooropa

예를 들어 getaddrinfo ()가 출력 매개 변수를 마지막에 넣는 방법과 같은 의미입니까? :-) 수천 개의 규칙이 있으며 원하는 것을 선택할 수 있습니다.
dkagedal

10

여기 사람들이 지금까지 언급 한 것을 잊어 버린 한 가지 사실은 구조체에 보통 패딩이 있다는 것입니다!

struct {
  short a;
  char b;
  short c;
  char d;
}

모든 문자는 1 바이트이고 모든 짧은 문자는 2 바이트입니다. 구조체는 얼마나 큽니까? 아니요, 6 바이트가 아닙니다. 적어도 더 일반적으로 사용되는 시스템에는 없습니다. 대부분의 시스템에서는 8입니다. 문제는 정렬이 일정하지 않고 시스템에 따라 다르므로 동일한 구조체는 시스템마다 다른 정렬 및 크기를 갖습니다.

패딩은 스택을 더 많이 소비 할뿐만 아니라 시스템 패딩 방법을 알고 앱에있는 모든 단일 구조체를보고 크기를 계산하지 않는 한 미리 패딩을 예측할 수 없다는 불확실성을 추가합니다 그것을 위해. 포인터를 전달하면 예측 가능한 공간이 필요하며 불확실성이 없습니다. 포인터의 크기는 시스템에 알려져 있으며 구조체의 모양에 관계없이 항상 동일하며 포인터 크기는 정렬되고 패딩이 필요하지 않은 방식으로 항상 선택됩니다.


2
그러나 패딩은 값이나 참조로 구조를 전달하는 데 의존하지 않고 존재합니다.
Ilya

2
@dkagedal : "다른 시스템에 다른 크기"의 어느 부분을 이해하지 못했습니까? 그것이 시스템에서 그런 방식이기 때문에 다른 시스템과 동일해야한다고 가정합니다. 이것이 바로 가치를 전달해서는 안되는 이유입니다. 시스템에서도 실패하도록 샘플을 변경했습니다.
Mecki

2
구조체 패딩에 대한 Mecki의 의견은 스택 크기가 문제가 될 수있는 임베디드 시스템과 관련이 있다고 생각합니다.
zooropa

1
인수의 반대 측면은 구조체가 간단한 구조체 (2 개의 프리미티브 유형을 포함하는) 인 경우 값을 전달하면 컴파일러가 레지스터를 사용하여 저글링 할 수 있다는 것입니다. 포인터를 사용하면 결과가 끝납니다 메모리가 느립니다. 이 수준이 중요하다면 대상 아키텍처에 따라 상당히 낮은 수준으로 유지됩니다.
kizzx2

1
구조체가 작거나 CPU에 많은 레지스터가 있고 (및 인텔 CPU가없는 경우), 데이터는 스택에서 끝나고 메모리이며 다른 메모리만큼 빠르거나 느립니다. 반면에 포인터는 항상 작으며 포인터와 포인터 자체는 일반적으로 더 자주 사용될 때 항상 레지스터에있게됩니다.
Mecki

9

나는 당신의 질문이 일들을 잘 요약했다고 생각합니다.

값으로 구조체를 전달할 때의 또 다른 이점은 메모리 소유권이 명시 적이라는 것입니다. 구조체가 힙에서 왔는지 누가 풀어야 할 책임이 있는지 궁금하지 않습니다.


9

매개 변수 및 반환 값으로 값별로 (너무 크지 않은) 구조체를 전달하는 것은 완벽하게 합법적 인 기술이라고 말하고 싶습니다. 물론 구조체가 POD 유형이거나 복사 의미가 잘 지정되어 있는지주의해야합니다.

업데이트 : 죄송합니다. C ++ 사고 제한이 있습니다. C에서 함수에서 구조체를 반환하는 것이 합법적이지 않은 시간을 회상하지만 그 이후로 아마도 변경되었을 것입니다. 사용하려는 모든 컴파일러가 연습을 지원하는 한 여전히 유효하다고 말하고 싶습니다.


내 질문은 C ++이 아니라 C에 관한 것입니다.
dkagedal

유용하지 않은 함수에서 구조체를 반환하는 것이 유효합니다 :)
Ilya

1
함수에서 데이터를 반환하기위한 오류 코드 및 매개 변수로 반환을 사용하는 llya의 제안이 마음에 듭니다.
zooropa

8

아무도 언급하지 않은 것이 있습니다.

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

(A)의 회원 const struct입니다 const,하지만 회원이 포인터가 (같은 경우 char *)이된다 char *const댄 오히려 const char *우리가 정말 원하는. 물론, 우리는const 는 의도의 문서이며, 이것을 위반하는 사람은 나쁜 코드를 작성하고 있지만, 일부의 경우 (특히 4 시간을 소비 한 사람들은 크래시).

대안은 그것을 만들고 struct const_blob { const char *c; size_t l }사용하는 것일 수도 있지만 다소 지저분 typedef합니다. 포인팅 포인터 와 동일한 이름 지정 체계 문제가 발생 합니다. 따라서 대부분의 사람들은 두 개의 매개 변수를 사용합니다 (이 경우 문자열 라이브러리를 사용하는 경우가 더 큼).


그렇습니다. 그것은 완전히 합법적이며 때로는하고 싶은 일입니다. 그러나 포인터를 const를 가리키는 포인터를 만들 수 없다는 것이 struct 솔루션의 제한 사항이라는 데 동의합니다.
dkagedal

struct const_blob해결책이 있는 불쾌한 const_blob점은 다른 구성원이 있더라도blob 은 "간접적 일관성"에서만 엄격한 앨리어싱 규칙의 목적에 따라 유형 struct blob*struct const_blob*구별되는 것으로 간주된다는 것입니다. 결과적으로, 코드가 a blob*에 a을 캐스팅하면 const_blob*한 유형을 사용하는 기본 구조에 대한 후속 쓰기는 다른 유형의 기존 포인터를 자동으로 무효화하여 사용하면 정의되지 않은 동작 (일반적으로 무해하지만 치명적일 수 있음)을 호출합니다. .
supercat 2016 년

5

http://www.drpaulcarter.com/pcasm/ 에있는 PC Assembly Tutorial의 150 페이지 에는 C가 함수가 구조체를 반환하도록 허용하는 방법에 대한 명확한 설명이 있습니다.

C는 또한 함수의 반환 값으로 구조 유형을 사용할 수 있습니다. 분명히 구조는 EAX 레지스터에 반환 될 수 없습니다. 다른 컴파일러는이 상황을 다르게 처리합니다. 컴파일러가 사용하는 일반적인 솔루션은 내부적으로 함수를 구조 포인터를 매개 변수로 사용하는 함수로 다시 작성하는 것입니다. 포인터는 호출 된 루틴 외부에 정의 된 구조에 리턴 값을 넣는 데 사용됩니다.

위의 진술을 확인하기 위해 다음 C 코드를 사용합니다.

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

"gcc -S"를 사용하여이 C 코드에 대한 어셈블리를 생성하십시오.

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

호출 전 스택은 다음을 생성합니다.

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

create를 호출 한 직후의 스택 :

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+

2
여기에는 두 가지 문제가 있습니다. 가장 분명한 것은 이것이 "C가 함수가 구조체를 반환하는 방법"을 전혀 설명하지 않는다는 것입니다. 32 비트 x86 하드웨어에서 수행 할 수있는 방법 만 설명합니다. 레지스터 수 등을 볼 때 가장 제한적인 아키텍처 중 하나입니다. 두 번째 문제는 C 컴파일러가 값을 반환하기위한 코드를 생성하는 방식입니다 (비 수출 또는 인라인 된 기능은 제외) ABI에 의해 지시됩니다. 그리고 인라인 함수는 아마도 구조체를 반환하는 것이 가장 유용한 곳 중 하나 일 것입니다.
dkagedal

수정 해 주셔서 감사합니다. 전화 컨벤션에 대한 자세한 내용은 en.wikipedia.org/wiki/Calling_convention을 참조하십시오.
Jingguo Yao

@dkagedal : 중요한 것은 x86이 이런 식으로 일을하는 것이 아니라, 어떤 플랫폼에서든 컴파일러가 모든 구조 유형의 리턴을 지원할 수있게하는 "유니버설"접근 방식이 있다는 것입니다. t 스택을 날려 버릴만큼 큰. 많은 플랫폼의 컴파일러가 일부 구조 유형 반환 값을 처리하기 위해보다 효율적인 다른 방법을 사용하지만 언어가 플랫폼이 최적으로 처리 할 수있는 유형으로 구조 반환 유형을 제한 할 필요는 없습니다.
supercat

0

값으로 구조체를 전달할 때의 장점 중 하나는 최적화 컴파일러가 코드를 더 잘 최적화 할 수 있다는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.