先週、Chandler Carruth は Carbon を発表しました。これは、彼らが過去 2 年間取り組んできた C++ の代替となる可能性があるものです。これは、便利なジェネリック、コンパイル時のインターフェイス/特性/概念、モジュールなど、現代の言語に期待される通常のクールな機能を備えています。など–しかし、私が最も興奮しているのは、パラメーターがそこに渡される方法に関する小さな詳細です.
これは私自身が過去に考えていたことであり、私の知る限り、これまでどの低レベル言語でも実行されたことはありませんでしたが、この概念には多くの可能性があります。私が話していることを説明しましょう。
Carbon のパラメーターの受け渡し
デフォルトでは、つまり、他に何も記述しない場合、Carbon パラメータは 08
に相当するものによって渡されます。 C++ で。
class Point
{
var x: i64;
var y: i64;
var z: i64;
}
fn Print(p : Point);
struct Point
{
std::uint64_t x, y, z;
};
void Print(const Point& p);
ただし、これはインポート部分です。コンパイラはそれを 16
に変換できます。 as-if ルールの下で。
fn Print(x : i32);
void Print(std::int32_t x);
…だから何?なぜ私はそれについてとても興奮しているのですか?
利点 1:パフォーマンス
26
で物事を渡す 常に良いですよね?結局、あなたはコピーを避けているのです!
true の間、参照は本質的にアセンブリ レベルのポインターです。これは、31
までに引数を渡すことを意味します。 レジスタをそのアドレスに設定します。つまり、
これは、レジスタに収まらない型、または自明でないコピー コンストラクターを持つ小さな型の唯一のオプションですが、適合する自明なコピー可能な型にはあまり適していません。
49
間のアセンブリを比較します 59
で引数を取る関数
[[gnu::noinline]] int add(const int& a, const int& b)
{
return a + b;
}
int foo()
{
return add(11, 42);
}
[[gnu::noinline]] int add(int a, int b)
{
return a + b;
}
int foo()
{
return add(11, 42);
}
68
を渡す必要はありません。 を参照してください!
したがって、Carbon ではそれについて考える必要がないのは本当に素晴らしいことです。コンパイラが正しいことを実行してくれるだけです。さらに、常に手動で実行できるとは限りません。
利点 #2:ジェネリック コードでの最適な呼び出し規約
C++ でジェネリック関数 print 関数を書きたいとします。型は、任意の高価なコピー コンストラクターを使用して任意に大きくすることができるため、71
を使用する必要があります。 汎用コードで。
template <typename T>
void Print(const T& obj);
ただし、これは小さくて安価な型の状況を悲観的にしています。これは残念なことです。関数のシグネチャと呼び出し規約は ABI の一部であるため、コンパイラが最適化で修正できるものでもありません。せいぜい、コンパイラはそれをインライン化し、呼び出し全体を省略できます。
もちろん、この問題を回避する方法はありますが、Carbon ではうまく機能します。これは素晴らしいことです。
しかし、私がこの機能に興奮している本当の理由は、メモリのロード/ストアをなくすこととは何の関係もありません.
利点 #3:コピーではないコピー
コンパイラが実行できる変換は、80
とまったく同じではないことに注意してください。 -> 95
後者は引数のコピーを作成します。必要に応じて、コピー コンストラクタとデストラクタを呼び出します。
Carbon では、これは当てはまりません:値は単純にレジスタに設定されます。呼び出された関数はパラメーターのデストラクタを呼び出さないため、呼び出し元はコピー コンストラクターを呼び出す必要はありません。 109
に相当する Carbon に対して有効であること .呼び出し元は単にレジスターを基礎となるポインター値に設定するだけで、呼び出し先はそれにアクセスできます.ここでは所有権の譲渡は発生しません.
これは (標準の) C++ ではできないことです。
利点 #4:アドレスのないパラメーター
その言語機能の結果について考えていた場合、次のような Carbon コードについて疑問に思うかもしれません:
fn Print(p : Point)
{
var ptr : Point* = &p;
…
}
コンパイラが 114
を渡すことを決定した場合 レジスタでは、それへのポインターを作成することはできません。したがって、コードはコンパイルされません。パラメーターのアドレスを取得してはなりません (125
を使用して宣言されている場合を除きます)。 キーワード)
追加の注釈がなければ、Carbon 関数のパラメーターはアドレスを持っていない可能性があるため、コンパイラーにアドレスを公開しません。これ 私がその機能にとても興奮している本当の理由です。
より正確なエスケープ分析
プログラマーはパラメーターのアドレスを取得できないため、エスケープ解析でそれらを考慮する必要はありません。たとえば、次の C++ コードでは、関数によって何が返されますか?
void take_by_ref(const int& i);
void do_sth();
int foo()
{
int i = 0;
take_by_ref(i);
i = 11;
do_sth();
return i;
}
さて、134
ただし、以下は 146
の有効な実装です。 と 157
:
int* ptr; // global variable
void take_by_ref(const int& i)
{
// i wasn't const to begin with, so it's fine
ptr = &const_cast<int&>(i);
}
void do_sth()
{
*ptr = 42;
}
突然 160
174
を返します – これは 100% 有効でした。そのため、コンパイラは 184
に格納されている値を個別に再読み込みする必要があります。 戻る前に逃げる .
Carbon では、これは不可能です 196
戻ってきてあなたを悩ませるような場所にこっそりアドレスを保存することはできません.そのため、208
エスケープせず、コンパイラは関数を最適化して 217
を返すことができます .
明示的なアドレス構文
次の C++ コードは大丈夫ですか?
class Widget
{
public:
void DoSth(const std::string& str);
};
Widget Foo()
{
Widget result;
std::string str = "Hello!";
result.DoSth(str);
return result;
}
場合によります。
223
関数ローカル文字列のアドレスを取得し、どこかに保存できます。その後、関数から返されたときに、ダングリング ポインターが含まれます。
Carbon では、これは不可能です。ウィジェットがポインタを保存したい場合、ポインタを受け入れる必要があります:
class Widget
{
fn DoSth[addr me : Self*](str : String*);
}
重要なのは、呼び出しコードもアドレスを取得する必要があるということです:
fn Foo() -> Widget
{
var result : Widget;
var str : String = "Hello";
result.DoSth(&str);
return result;
}
呼び出しの余分な構文により、ここで何か問題が発生している可能性が非常に明確になります。
同じ理由で、Google C++ スタイル ガイドでは、このような状況で C++ コードのポインターを要求していました。これには、236
を渡すことができるという不幸な副作用があります。 パラメータに
将来の言語拡張
パラメータでは、253
はアドレスが取れないパラメータで、267
は、アドレスを持つパラメーターです。同じ原則を他の状況にも適用できます。たとえば、次のクラスを考えてみましょう:
class Birthday
{
var year : i32;
var month : i8;
var day : i8;
}
class Person
{
var birthday : Birthday;
var number_of_children : i8;
}
C++ のサイズが 276
であるため、Carbon がデータ レイアウトに関して同じ規則に従うと仮定します。 は 8 バイトです (284
の場合は 4 バイト) 、290
の場合は 1 、302
の場合は 1 最後に 2 つのパディング バイト)、および 312
のサイズ 12 バイト (321
の場合は 8 バイト) 、332
の場合は 1 バイト 、およびパディング用の 3)。
より最適なレイアウトは 349
を排除します メンバーを 358
にインライン化します :
class Person
{
var birthday_year : i32;
var birthday_month : i8;
var birthday_day : i8;
var number_of_children : i8;
}
今、369
のサイズ 375
のため、わずか 8 バイトです。 以前はパディング バイトだったものに格納できます。
これはコンパイラが実行できる最適化ですか?
別の 388
を保持する必要があるため、そうではありません。 サブオブジェクト:誰かが 398
のアドレスを取得できる
ただし、 401
がないことで示される、アドレスを取得できないメンバー変数を想像することはできます。 :
class Person
{
birthday : Birthday;
number_of_children : i8;
}
これで、コンパイラは自由にレイアウトを変更し、構造体メンバーをインライン化し、それらをシャッフルします。411
のアドレスを取得することに注意してください。 (および他のメンバー) は問題ありません:422
で宣言されています 430
の隣にあるとは限りませんが、メモリに連続して格納されます。 と 445
.458
467
以外 メンバーは自由に混合できます。
同様に、構造体の配列を配列の構造体に変換する最適化も無効です。最初のレイアウトでは、アドレスを持つメモリの 1 つの連続したチャンクに個々の構造体が存在しますが、2 番目のレイアウトでは構造体のメンバーが分割されています。要素のアドレスを取得できない配列がありますが、これは観察できるものではありません。
最後に、それをローカル変数に拡張すると、基本的に C の register キーワードが有効になります。レジスタに安全に存在できるアドレスのないローカル変数。最新のオプティマイザでは必要ありませんが、コンパイラが考慮する必要がない場合は、それでも作業は少なくなります。さらに重要なことは、読者に対して意図を文書化することです。
結論
アドレスを取得できないエンティティを作成することは、多くの可能性を秘めたシンプルな機能です。レイアウトを観察できないため、レイアウトを変更するための多くの最適化が可能になり、エスケープ分析が簡素化され、パラメーターの受け渡しが最適化されます。
多くの場合、それは実際には制限ではありません。何かのアドレスを実際に取得する必要がある頻度はどれくらいですか?これらのいくつかの状況を追加のキーワードでマークしても、費用はかかりません.
C++にもそれがあればいいのにと思いますが、参照を取る関数では機能しないため、最初から言語がC++を中心に設計されていない限り、それらは役に立たなくなります.
これがまさに Carbon の出番です。