web-dev-qa-db-ja.com

値で構造体を渡すまたは返すのはいつですか?

構造体は、Cで値によって渡されるか返されるか、参照によって(ポインターを介して)渡されるか返されます。

一般的なコンセンサスは、ほとんどの場合、前者をペナルティなしで小さな構造体に適用できるということです。 構造体を直接返すことをお勧めする場合はありますか? および ポインタを渡すのではなく、Cで値で構造体を渡すことにマイナス面はありますか?

そして、参照解除を回避することは、速度と明確さの両方の観点から有益です。しかし、何がsmallとしてカウントされますか?これは小さな構造体であることに全員が同意できると思います。

struct Point { int x, y; };

相対免責で値渡しできること:

struct Point sum(struct Point a, struct Point b) {
  return struct Point { .x = a.x + b.x, .y = a.y + b.y };
}

そして、そのLinuxのtask_structは大きな構造体です。

https://github.com/torvalds/linux/blob/b953c0d234bc72e8489d3bf51a276c5c4ec85345/include/linux/sched.h#L1292-1727

(特にこれらの8Kカーネルモードスタックでは!)すべてのコストでスタックを配置することを避けたいと思います。しかし、中間のものはどうですか?レジスターより小さい構造体は問題ないと思います。しかし、これらはどうですか?

typedef struct _mx_node_t mx_node_t;
typedef struct _mx_Edge_t mx_Edge_t;

struct _mx_Edge_t {
  char symbol;
  size_t next;
};

struct _mx_node_t {
  size_t id;
  mx_Edge_t Edge[2];
  int action;
};

構造体が値によって安全に渡せるほど小さいかどうかを判断するための最良の経験則は何ですか?深い再帰)?

最後に、プロファイルを作成する必要があると言わないでください。私は怠tooすぎる/それをさらに調査する価値がないときに使用するヒューリスティックを求めています。

編集:これまでの回答に基づいて、2つのフォローアップの質問があります:

  1. 構造体が実際にsmallerである場合はどうでしょうか?

  2. 浅いコピーが望ましい動作である場合(呼び出された関数はとにかく浅いコピーを実行します)?

編集:私は実際に私の質問の他の質問をリンクするので、これが可能な重複としてマークされた理由がわかりません。 small構造体を構成するものを明確にするよう求めており、ほとんどの場合、構造体は参照渡しする必要があることを十分に認識しています。

41
Kaiting Chen

小さな組み込みアーキテクチャ(8/16ビター)-alwaysポインターによる受け渡し。重要な構造はこのような小さなレジスターに収まらないため、これらのマシンは一般にレジスター不足です。

PCのようなアーキテクチャ(32ビットプロセッサと64ビットプロセッサ)-値で構造体を渡すことはsizeof(mystruct_t) <= 2*sizeof(mystruct_t*)であり、その関数には多くの(通常3マシンワード以上の)他の引数はありません。このような状況では、典型的な最適化コンパイラーは、レジスターまたはレジスターのペアで構造体を渡したり返したりします。ただし、x86-32では、x86-32コンパイラーが対処しなければならない並外れたレジスター圧力のために、このアドバイスは多額の塩分を使用して行う必要があります-ポインターの受け渡しは、レジスターのこぼれや塗りつぶしが少ないため、依然として高速です。

一方、PCライクで値によって構造を返すことも同じ規則に従います。構造がポインターによって返される場合、記入される構造はpassedである必要があるという事実を除きます。ポインタでも同様です-そうでなければ、呼び出し先と呼び出し元は、その構造のメモリを管理する方法について同意しなければならないままです。

16
LThode

私の経験、ほぼ40年間のリアルタイム組み込み、最後の20年はCを使用しました。最良の方法は、ポインターを渡すことです。

どちらの場合でも、構造体のアドレスをロードする必要があります。次に、対象フィールドのオフセットを計算する必要があります...

構造体全体を渡すときに、参照渡しではない場合、

  1. スタックに配置されていません
  2. 通常、memcpy()の非表示呼び出しによってコピーされます
  3. 現在「予約」されており、プログラムの他の部分では使用できないメモリのセクションにコピーされます。

構造体が値によって返される場合にも同様の考慮事項があります。

ただし、コンパイルレジスタで特定のレベルの最適化が使用されている場合は特に、作業レジスタに2つまで完全に保持できる「小さな」構造体がそれらのレジスタに渡されます。

「小さい」と見なされるものの詳細は、コンパイラと基盤となるハードウェアアーキテクチャによって異なります。

23
user3629249

質問の引数を渡す部分は既に回答されているため、返される部分に焦点を当てます。

IMOを実行する最善の方法は、構造体または構造体へのポインタをまったく返さず、「結果構造体」へのポインタを関数に渡すことです。

void sum(struct Point* result, struct Point* a, struct Point* b);

これには次の利点があります。

  • result構造体は、呼び出し側の裁量で、スタックまたはヒープのいずれかに存在できます。
  • 呼び出し側が結果の構造体を割り当てて解放する責任があることは明らかなので、所有権の問題はありません。
  • 構造は、必要な長さよりも長い場合や、より大きな構造に埋め込まれている場合があります。
7
alain

関数との間で構造体がどのように渡されるかは、アプリケーションバイナリインターフェイス(ABI)およびターゲットプラットフォーム(CPU/OS、一部のプラットフォームでは以上の場合があるABIに含まれるPCS)に依存します。 1つのバージョン)。

If PCSは実際にレジスタで構造体を渡すことを許可します。これは、サイズだけでなく、引数リスト内の位置と先行する引数のタイプにも依存します。インスタンス用のARM-PCS(AAPCS)は、引数がいっぱいになるまで最初の4つのレジスタに引数をパックし、引数が分割されることを意味する場合でも、さらにデータをスタックに渡します(興味がある場合はすべて、文書はARMから無料でダウンロードできます) )。

返される構造体について、レジスタを介して渡されない場合、ほとんどのPCSは呼び出し側によってスタック上のスペースを割り当て、構造体へのポインターを呼び出し先に渡します(暗黙的なバリアント)。これは、呼び出し元のローカル変数と同じであり、呼び出し先に対して明示的にポインタを渡します。ただし、暗黙的なバリアントの場合、暗黙的に割り当てられた構造体への参照を取得する方法がないため、結果を別の構造体にコピーする必要があります。

一部のPCSは引数構造体に対して同じことを行いますが、他のPCSはスカラーと同じメカニズムを使用するだけです。いずれにせよ、そのような最適化は、本当に必要であるとわかるまで延期します。ターゲットプラットフォームのPCSも読んでください。コードは、異なるプラットフォームでさらにパフォーマンスが低下する可能性があることに注意してください。

注:構造体をグローバルtempに渡すことは、スレッドセーフではないため、最新のPCSでは使用されません。ただし、一部の小さなマイクロコントローラーアーキテクチャでは、これが異なる場合があります。ほとんどの場合、小さなスタック(S08)または制限された機能(PIC)のみがあります。しかし、これらのほとんどの場合、構造体もレジスタに渡されず、ポインタによるパスが強く推奨されます。

元の不変性のためだけの場合:const mystruct *ptrを渡します。少なくとも構造体への書き込み時に警告を出すconstを捨てない限り。ポインター自体も定数にすることができます:const mystruct * const ptr

そのため、経験則はありません。あまりにも多くの要因に依存します。

構造体を引数として関数への引数として渡すことに関しては、値による参照ではなく、実際の最良の経験則は、値による渡しを避けることです。ほとんどの場合、リスクは利益を上回ります。

完全を期すために、値で構造体を渡す/返すときに、いくつかのことが起こることを指摘します。

  1. 構造体のすべてのメンバーがスタックにコピーされます
  2. 構造体を値で返す場合、再び、すべてのメンバーが関数のスタックメモリから新しいメモリ位置にコピーされます。
  3. 操作はエラーが発生しやすい-構造体のメンバーがポインターである場合、一般的なエラーは、ポインターで操作しているため、値でパラメーターを渡すことが安全であると仮定することです-これはバグを見つけるのが非常に困難です。
  4. 関数が入力パラメーターの値を変更し、入力が値渡しの構造体変数である場合、常に値ごとに構造体変数を返すことを覚えておく必要があります(これは何度も見ました)。これは、構造体メンバーをコピーする時間を2倍にすることを意味します。

ここで、構造体のサイズに関して十分に小さいものに到達する-値で渡すのが「価値」であるように、それはいくつかのことに依存します:

  1. 呼び出し規約:その関数を呼び出すときにコンパイラが自動的にスタックに保存するもの(通常はいくつかのレジスタの内容です)。このメカニズムを利用して構造メンバーをスタックにコピーできる場合、ペナルティはありません。
  2. 構造体のメンバーのデータ型:マシンのレジスターが16ビットで、構造体のメンバーのデータ型が64ビットの場合、明らかに1つのレジスターに収まらないため、1つのコピーに対して複数の操作を実行する必要があります。
  3. マシンが実際に持っているレジスタの数:1つのメンバー(char(8ビット))だけの構造があると仮定します。パラメーターを値または参照(理論上)で渡すときに、同じオーバーヘッドが発生するはずです。しかし、潜在的にもう1つの危険があります。アーキテクチャに個別のデータおよびアドレスレジスタがある場合、値で渡されるパラメーターは1つのデータレジスタを占有し、参照で渡されるパラメーターは1つのアドレスレジスタを占有します。パラメータを値で渡すと、通常はアドレスレジスタよりも多く使用されるデータレジスタに圧力がかかります。そして、これはスタック上に流出を引き起こす可能性があります。

結論-構造体を値で渡すことは問題ないかと言うのは非常に困難です。それをしないだけの方が安全です:)

5
Pandrei

注:何らかの理由で重複する理由。

値で渡す/返す場合:

  1. オブジェクトは、intdouble、ポインターなどの基本型です。
  2. オブジェクトのバイナリコピーを作成する必要があります-オブジェクトは大きくありません。
  3. 速度は重要であり、値渡しはより高速です。
  4. オブジェクトは概念的には小さな数字です

    struct quaternion {
      long double i,j,k;
    }
    struct pixel {
      uint16_t r,g,b;
    }
    struct money {
      intmax_t;
      int exponent;
    }
    

オブジェクトへのポインターを使用する場合

  1. 値または値へのポインターのどちらが優れているかわからない-これがデフォルトの選択です。
  2. オブジェクトが大きい。
  3. 速度が重要であり、オブジェクトへのポインタによる受け渡しはより高速です。
  4. スタックの使用は重要です。 (厳密には、これは場合によっては値で有利になる可能性があります)
  5. 渡されたオブジェクトへの変更が必要です。
  6. オブジェクトにはメモリ管理が必要です。

    struct mystring {
      char *s;
      size_t length;
      size_t size;
    }
    

注:Cでは、参照によって実際に渡されるものはないことを思い出してください。ポインターの値がコピーされて渡されるため、ポインターを渡すことも値によって渡されます。

概念的にコードを理解する方が簡単なので、値でintまたはpixelである数字を渡すことを好みます。アドレスで数値を渡すことは概念的に少し難しいです。より大きな数値オブジェクトの場合、アドレスで渡すのはfasterです。

アドレスが渡されたオブジェクトは、restrictを使用して、オブジェクトにオーバーラップしないことを関数に通知できます。

3
chux

典型的なPCでは、かなり大きな構造(数十バイト)でもパフォーマンスは問題になりません。したがって、他の基準、特にセマンティクスが重要です。実際にコピーを作成しますか?または、同じオブジェクト上、例えばリンクリストを操作するとき?ガイドラインは、コードを読みやすく保守しやすくするために、最も適切な言語構造で目的のセマンティクスを表現することです。

とはいえ、パフォーマンスに影響がある場合は、思っているほど明確ではないかもしれません。

  • Memcpyは高速であり、メモリの局所性(スタックに適しています)がデータサイズよりも重要な場合があります:スタックの値で構造体を渡して返す場合、コピーはすべてキャッシュで発生する可能性があります。また、戻り値の最適化では、返されるローカル変数の冗長なコピーを回避する必要があります(これは、20年または30年前に素朴なコンパイラが行っていました)。

  • ポインタを渡すと、メモリロケーションにエイリアスが導入され、それ以上効率的にキャッシュできなくなります。現代の言語は、すべてのデータが副作用から隔離されているため、多くの場合、より価値志向です。これにより、コンパイラの最適化能力が向上します。

一番下の行はyesです。問題が発生した場合を除き、より便利または適切な場合は、値渡しを気軽に行ってください。さらに高速かもしれません。

1