Bir işaretçiyi iletmek yerine yapıları C değerine göre geçirmenin dezavantajları var mı?


157

Bir işaretçiyi iletmek yerine yapıları C değerine göre geçirmenin dezavantajları var mı?

Yapı büyükse, çok sayıda veri kopyalamanın performans açısından açık bir yönü vardır, ancak daha küçük bir yapı için, temel olarak bir işleve birkaç değer iletmekle aynı olmalıdır.

Dönüş değerleri olarak kullanıldığında daha da ilginç olabilir. C'nin işlevlerden yalnızca tek dönüş değerleri vardır, ancak genellikle birkaçına ihtiyacınız vardır. Yani basit bir çözüm onları bir yapıya koymak ve geri vermek.

Buna karşı veya buna karşı bir neden var mı?

Burada bahsettiğim herkese açık olmayabileceğinden, basit bir örnek vereceğim.

C dilinde programlama yapıyorsanız, er ya da geç aşağıdaki gibi işlevler yazmaya başlayacaksınız:

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

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

Bu bir sorun değil. Tek sorun, tüm fonksiyonlarda aynı kuralı kullanmanız için parametrelerin hangi sırada olması gerektiği konusunda iş arkadaşınızla anlaşmanız gerektiğidir.

Ancak aynı tür bilgileri geri döndürmek istediğinizde ne olur? Genellikle böyle bir şey alırsınız:

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

Bu iyi çalışıyor, ancak çok daha sorunlu. Bir dönüş değeri, bu uygulamada olmaması dışında bir dönüş değeridir. Yukarıdan, get_data fonksiyonunun neye işaret ettiğine bakmasına izin verilmediğini söylemenin bir yolu yoktur. Ve derleyiciyi bu işaretçiden bir değerin gerçekten döndürüldüğünü kontrol eden hiçbir şey yoktur. Gelecek ay, bir başkası kodu doğru anlamadan değiştirdiğinde (belgeleri okumadığından dolayı?) Fark etmeden kırılır veya rastgele çökmeye başlar.

Yani, önerdiğim çözüm basit yapı

struct blob { char *ptr; size_t len; }

Örnekler şu şekilde yeniden yazılabilir:

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();

Nedense, çoğu insan içgüdüsel olarak examine_data bir yapı blob için bir işaretçi almak istiyorum düşünüyorum, ama neden görmüyorum. Hala bir işaretçi ve bir tam sayı alır, birlikte gittikleri çok daha açıktır. Ve get_data durumunda, uzunluk için herhangi bir girdi değeri olmadığından ve döndürülen bir uzunluk olması gerektiğinden, daha önce tanımladığım şekilde karıştırmak imkansızdır.


Değeri void examine data(const struct blob)için yanlış.
Chris Lutz

Teşekkürler, değişken adı içerecek şekilde değiştirildi.
dkagedal

1
"Yukarıdan, get_data işlevinin neye işaret ettiğine bakmasına izin verilmediğini söylemenin bir yolu yoktur. Derleyiciye bu işaretçiden bir değerin gerçekten döndürüldüğünü kontrol etmesini sağlayan hiçbir şey yoktur." - bu benim için hiç mantıklı değil (belki örneğinizin bir fonksiyonun dışında görünen son iki satır nedeniyle geçersiz kod olması); lütfen biraz açıklayabilir misin?
Adam Spires

2
Fonksiyonun altındaki iki satır, fonksiyonun nasıl çağrıldığını göstermek için vardır. İşlev imzası, uygulamanın yalnızca işaretçiye yazması gerektiğine dair bir ipucu vermez. Derleyicinin bir değerin işaretçiye yazıldığını doğrulaması gerektiğini bilmesinin bir yolu yoktur, bu nedenle dönüş değeri mekanizması yalnızca belgelerde açıklanabilir.
dkagedal

1
İnsanların bunu C'de daha sık yapmamalarının ana nedeni tarihseldir. C89'dan önce, yapıları değere göre geçemez veya geri veremezdiniz , bu nedenle C89'dan önce gelen ve mantıksal olarak yapması gereken (örneğin gettimeofday) tüm sistem arayüzleri bunun yerine işaretçileri kullanır ve insanlar bunu örnek olarak alır.
zwol

Yanıtlar:


202

Küçük yapılar için (örn. Nokta, doğrultu) değere göre geçmek mükemmel şekilde kabul edilebilir. Ancak, hızın yanı sıra, büyük yapıları değere göre geçirme / döndürme konusunda dikkatli olmanızın bir başka nedeni daha vardır: Yığın alanı.

Belleğin çok değerli olduğu gömülü sistemler için bir çok C programlama vardır ve yığın boyutları KB veya hatta Bayt cinsinden ölçülebilir ... Yapıları değere göre geçirir veya döndürürseniz, bu yapıların kopyaları yerleştirilir yığın, potansiyel olarak bu sitenin adını almıştır duruma neden ...

Aşırı yığın kullanımı gibi görünen bir uygulama görürsem, değere göre aktarılan yapılar ilk aradığım şeylerden biridir.


2
" Yapıları değere göre geçiriyorsanız veya döndürüyorsanız, bu yapıların kopyaları yığına yerleştirilir" Bunu yapan araç zincirlerini braindead olarak adlandırırım . Evet, birçoğunun bunu yapması üzücü ama C standardının gerektirdiği bir şey değil. Aklı başında bir derleyici hepsini optimize eder.
Monica

1
@KubaOber Bu yüzden sık sık yapılmıyor: stackoverflow.com/questions/552134/…
Roddy

1
Küçük bir yapıyı büyük bir yapıdan ayıran kesin bir çizgi var mı?
Josie Thompson

63

Daha önce değinilmemiş olan bunun yapılmamasının bir nedeni, bunun ikili uyumluluğun önemli olduğu bir soruna neden olabilmesidir.

Kullanılan derleyiciye bağlı olarak, yapılar derleyici seçeneklerine / uygulamasına bağlı olarak yığın veya kayıtlardan geçirilebilir

Bkz. Http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-yapı-dönüş

-freg-yapı-dönüş

İki derleyici aynı fikirde olmazsa işler patlayabilir. Bunu yapmamanın temel nedenlerinin resmedilmeye gerek yok, yığın tüketimi ve performans nedenleridir.


4
Aradığım cevap buydu.
dkagedal

2
Doğru, ancak bu seçenekler her bir geçiş değeriyle ilgili değil. tamamen farklı bir şey olan geri dönen yapılarla ilgilidir . Bir şeyleri referans olarak döndürmek genellikle kendinizi her iki ayağa vurmanın kesin bir yoludur. int &bar() { int f; int &j(f); return j;};
Roddy

19

İçin gerçekten montaj arazi içine derin kazmak, bir ihtiyacı bu soruya cevap:

(Aşağıdaki örnek x86_64 üzerinde gcc kullanır. Herkes MSVC, ARM vb. Gibi başka mimariler ekleyebilir.)

Örnek programımızı yapalım:

// 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;
}

Tam optimizasyonlarla derleyin

gcc -Wall -O3 foo.c -o foo

Meclise bakın:

objdump -d foo | vim -

Elde ettiğimiz bu:

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)

noplPedler hariç give_two_doubles(), 27 bayt give_point(), 29 bayt vardır. Öte yandan, give_point()bir daha az talimat verirgive_two_doubles()

İlginç olan şey, derleyicinin movdaha hızlı SSE2 varyantlarına optimize edebildiğini fark etmemiz movapdve movsd. Dahası, give_two_doubles()aslında verileri bellekten içeri ve dışarı taşır, bu da işleri yavaşlatır.

Görünüşe göre bunların çoğu gömülü ortamlarda (C'nin oyun alanının günümüzde çoğu zaman olduğu yerde) geçerli olmayabilir. Bir montaj sihirbazı değilim, bu yüzden herhangi bir yorum hoş geldiniz!


6
Büyük bir fark göstermedikçe veya tahmin edilmesi zor sıçramaların sayısı gibi daha ilginç yönleri sayamadığınız sürece, talimatların sayısını saymak o kadar da ilginç değildir. Gerçek performans özellikleri, talimat sayısından çok daha incedir. .
dkagedal

6
@dkagedal: Doğru. Geçmişe baktığımda kendi cevabımın çok kötü yazılmış olduğunu düşünüyorum. Her ne kadar talimatların sayısına çok fazla odaklanmasam da (size bu izlenimi veren şeyi bilmiyorum: P), asıl önemli olan, değere göre yapıyı geçmenin küçük türler için referansla geçilmeye tercih edilmesiydi. Her neyse, değere göre geçmek tercih edilir çünkü daha basittir (ömür boyu hokkabazlık yok, birisinin verilerinizi değiştirdiği veya consther zaman değiştirdiği için endişelenmenize gerek yok ) ve ben de değere göre kopyalamada çok fazla performans cezası (kazanç yoksa) olmadığını buldum , birçok kişinin inanabileceğinin aksine.
kizzx2

15

Basit bir çözüm, dönüş değeri olarak bir hata kodu ve işlevdeki bir parametre olarak her şeyi döndürecektir,
Bu parametre elbette bir yapı olabilir, ancak değere göre geçen herhangi bir avantaj görmüyorum, sadece bir işaretçi gönderdi.
Yapıyı değere göre geçirmek tehlikelidir, geçmekte olduğunuz şeylere çok dikkat etmeniz gerekir, C'de kopya oluşturucu olmadığını unutmayın, yapı parametrelerinden biri bir işaretçi ise işaretçi değeri kopyalanır, çok kafa karıştırıcı ve zor olabilir sürdürmek.

Sadece cevabı tamamlamak için ( Roddy'ye tam kredi ) yığın kullanımı, değere göre yapıyı geçmemek için başka bir nedendir, inanıyorum ki yığın taşması hata ayıklama gerçek PITA.

Yorum yapmak için tekrar et:

Yapıyı işaretçi olarak geçirmek, bazı varlıkların bu nesne üzerinde bir mülkiyete sahip olduğu ve ne zaman ve ne zaman serbest bırakılması gerektiği konusunda tam bilgiye sahip olduğu anlamına gelir. Yapıyı değere göre geçirmek, yapının iç verilerine gizli bir referans oluşturur (başka yapılara işaretçiler vb.) Bu bakımını yapmak zordur (mümkün ama neden?).


6
Ama bir işaretçiyi geçmek sadece onu bir yapıya koyduğunuz için daha "tehlikeli" değildir, bu yüzden satın almıyorum.
dkagedal

İşaretçi içeren bir yapının kopyalanmasında büyük nokta. Bu nokta çok açık olmayabilir. Neye atıfta bulunduğunu bilmeyenler için derin kopya ile sığ kopya arasında arama yapın.
zooropa

1
C işlevi kurallarından biri, giriş parametrelerinden önce çıkış parametrelerinin listelenmesini sağlamaktır, örneğin int func (char * out, char * in);
zooropa

Yani getaddrinfo () nasıl çıktı parametresini son koyar gibi? :-) Bin set kongre var ve hangisini istersen seçebilirsin.
dkagedal

10

Buradaki insanların şimdiye kadar bahsetmeyi unuttukları bir şey (ya da gözden kaçırdım), yapıların genellikle bir dolguya sahip olmasıdır!

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

Her karakter 1 bayt, her kısa 2 bayttır. Yapı ne kadar büyük? Hayır, 6 bayt değil. En azından daha sık kullanılan sistemlerde değil. Çoğu sistemde 8 olacaktır. Sorun, hizalama sabit değil, sisteme bağlıdır, bu nedenle aynı yapı farklı sistemlerde farklı hizalama ve farklı boyutlara sahip olacaktır.

Sadece bu dolgu, yığınızı daha fazla yiyemez, aynı zamanda sistem pedlerinizi nasıl bildiğini ve uygulamanızdaki her bir yapıya nasıl bakacağınızı ve boyutunu hesaplayamazsanız, dolguları önceden tahmin edememe belirsizliğini de ekler. onun için. Bir işaretçiyi geçmek tahmin edilebilir miktarda alan gerektirir - belirsizlik yoktur. İşaretçinin boyutu sistem için bilinir, yapının neye benzediğine bakılmaksızın her zaman eşittir ve işaretçi boyutları her zaman hizalanacak ve dolgu gerektirmeyecek şekilde seçilir.


2
Evet, ancak dolgu, yapıyı değere veya referansa göre geçirmeye bağımlı değildir.
Ilya

2
@dkagedal: "Farklı sistemlerde farklı boyutların" hangi kısmını anlamadınız? Sisteminizde bu şekilde olduğu için, başka herhangi biri için aynı olması gerektiğini varsayıyorsunuz - tam da bu yüzden değere göre geçmemelisiniz. Numune değiştirildi, böylece sisteminizde de başarısız oldu.
Mecki

2
Mecki'nin yapı dolgusu hakkındaki yorumlarının özellikle yığın boyutunun sorun olabileceği gömülü sistemler için geçerli olduğunu düşünüyorum.
zooropa

1
Sanırım argümanın çevirme tarafı, eğer yapınız basit bir yapı ise (birkaç ilkel tip içeren), değere göre geçmek derleyicinin kayıtları kullanarak dengelemesini sağlayacaktır - oysa işaretçiler kullanırsanız, işler sonuçlanır. daha yavaş olan bellek. Bu oldukça düşük seviyeli olur ve bu çerezlerden herhangi biri önemliyse, hedef mimarinize bağlıdır.
kizzx2

1
Yapınız küçük değilse veya CPU'nuzda çok sayıda kayıt yoksa (ve Intel CPU'larda bulunmuyorsa), veriler yığının üzerinde biter ve bu da bellek ve diğer bellekler kadar hızlı / yavaştır. Öte yandan bir işaretçi her zaman küçüktür ve yalnızca bir işaretçi ve işaretçinin kendisi genellikle daha sık kullanıldığında bir kayıt defterine dönüşür.
Mecki

9

Sorunuzun bir şeyleri oldukça iyi özetlediğini düşünüyorum.

Yapıları değere göre geçirmenin bir diğer avantajı da bellek sahipliğinin açık olmasıdır. Yapının öbekten olup olmadığı ve serbest bırakılmasından kimin sorumlu olduğu hakkında bir merak yoktur.


9

Hem parametre olarak hem de dönüş değerleri olarak değere göre (çok büyük olmayan) yapıları geçirmenin mükemmel bir meşru teknik olduğunu söyleyebilirim. Elbette, yapının bir POD tipi olduğuna ya da kopya semantiğinin iyi belirtildiğine dikkat etmek gerekir.

Güncelleme: Üzgünüm, C ++ düşünme başlığım vardı. Bir işlevden bir yapı döndürmenin C'de yasal olmadığı bir zamanı hatırlıyorum, ancak bu muhtemelen o zamandan beri değişti. Kullanmayı beklediğiniz tüm derleyiciler uygulamayı desteklediği sürece bunun geçerli olduğunu söyleyebilirim.


Sorumun C ++ ile değil C ile ilgili olduğunu unutmayın.
dkagedal

Yapıyı işlevden döndürmek geçerli değil yararlı değil :)
Ilya

1
Ben llya'nın fonksiyondan veri döndürmek için bir hata kodu ve parametreler olarak dönüş kullanmak için öneri gibi.
zooropa

8

İşte kimsenin bahsetmediği bir şey:

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
}

Bir üyeleri const structvardır const, ama bu eleman bir işaretçi (gibi ise char *), o olur char *constyerine const char *biz gerçekten istiyoruz. Tabii ki,const niyetin dokümantasyonu olduğunu ve bunu ihlal eden herkesin kötü kod yazdığını (ki bunlar), bu bazıları için yeterince iyi değil (özellikle de bir nedeni araştırmak için dört saat harcayanlar) ) çökmesine.

Alternatif bir yapmak struct const_blob { const char *c; size_t l }ve kullanmak olabilir, ama bu oldukça dağınık - typedefişaretçilerle sahip olduğum aynı adlandırma düzeni sorununa giriyor . Bu nedenle, çoğu insan sadece iki parametreye bağlı kalır (veya bu durumda bir dize kütüphanesi kullanarak daha olasıdır).


Evet, tamamen yasal ve bazen yapmak istediğiniz bir şey. Ancak katılıma işaret ettikleri göstergeleri yapamayacağınız yapısal çözümün bir sınırlaması olduğunu kabul ediyorum.
dkagedal

struct const_blobÇözümü ile kötü bir gotcha sadece "dolaylı-darlık" const_blobfarklı üyeler olsa bile blob, bir tür struct blob*için struct const_blob*sıkı takma kural amaçları farklı olarak kabul edilecektir. Sonuç olarak, kod a ' blob*ya atarsa const_blob*, bir tür kullanan altta yatan yapıya sonradan yapılacak herhangi bir yazma, diğer türdeki mevcut işaretçileri sessizce geçersiz kılacaktır, böylece herhangi bir kullanım Tanımsız Davranışı (genellikle zararsız olabilir, ancak ölümcül olabilir) çağıracaktır. .
supercat

5

Http://www.drpaulcarter.com/pcasm/ adresindeki PC Meclisi Eğiticisinin 150. sayfası , C'nin bir işlevin bir yapıyı döndürmesine nasıl izin verdiğine ilişkin net bir açıklamaya sahiptir:

C ayrıca bir fonksiyon tipinin dönüş değeri olarak bir yapı tipinin kullanılmasına izin verir. Açıkçası EAX kaydında bir yapı döndürülemez. Farklı derleyiciler bu durumu farklı şekilde ele alır. Derleyicilerin kullandığı yaygın bir çözüm, işlevi bir yapı işaretçisini parametre olarak alan işlev olarak dahili olarak yeniden yazılmasıdır. İşaretçi, dönüş değerini çağrılan rutinin dışında tanımlanan bir yapıya koymak için kullanılır.

Yukarıdaki ifadeyi doğrulamak için aşağıdaki C kodunu kullanın:

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;
}

Bu C kodu parçası için montaj oluşturmak için "gcc -S" kullanın:

    .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

Çağrıdan önceki yığın oluşturur:

        +---------------------------+
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 öğesini çağırdıktan hemen sonra yığın:

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

2
Burada iki problem mevcut. En bariz olanı, bunun "C bir fonksiyonun bir yapıyı geri döndürmesine nasıl izin verdiğini" hiç açıklamamasıdır. Bu sadece 32 bit x86 donanım üzerinde nasıl yapılacağını açıklar, hangi kayıt sayısı vb. Baktığınızda en sınırlı mimarilerden biri olur. İkinci sorun C derleyicileri dönen değerler için kod oluşturma yolu ABI tarafından belirlenir (dışa aktarılmamış veya satır içi işlevler hariç). Bu arada, eğik fonksiyonlar muhtemelen geri dönen yapıların en yararlı olduğu yerlerden biridir.
dkagedal

Düzeltmeler için teşekkürler. Arama kuralının tam bir ayrıntısı için, en.wikipedia.org/wiki/Calling_convention iyi bir referanstır.
Jingguo Yao

@dkagedal: Önemli olan sadece x86'nın işleri bu şekilde yapması değil, aynı zamanda herhangi bir platform için derleyicilerin herhangi bir yapı türünün getirisini desteklemesine izin verecek bir "evrensel" yaklaşım (yani bu) olması t Yığını üfleyecek kadar büyük. Birçok platform için derleyiciler, bazı yapı tipi dönüş değerlerini işlemek için daha verimli başka araçlar kullanacak olsa da, dilin yapı dönüş türlerini platformun en iyi şekilde işleyebileceği biçimlerle sınırlamasına gerek yoktur.
supercat

0

Yapılarınızı değere göre geçirmenin bir avantajına işaret etmek istiyorum, optimize edici bir derleyici kodunuzu daha iyi optimize edebilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.