Cでのパン関数の入力

Cには柔軟性がないという評判があります。 しかし、気に入らない場合はCで関数の引数を並べ替えることができることを知っていますか?







#include <math.h> #include <stdio.h> double DoubleToTheInt(double base, int power) { return pow(base, power); } int main() { //          double (*IntPowerOfDouble)(int, double) = (double (*)(int, double))&DoubleToTheInt; printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100)); printf("(0.99)^100: %lf \n", IntPowerOfDouble(100, 0.99)); }
      
      





IntPowerOfDouble



関数IntPowerOfDouble



存在IntPowerOfDouble



ないため、このコードは実際にはIntPowerOfDouble



関数を定義IntPowerOfDouble



ません。 これはDoubleToTheInt



を指す変数ですが、 int



型の引数がdouble



型の引数の前に来ることをDoubleToTheInt



する型を持ちます。







IntPowerOfDouble



同じ順序で引数IntPowerOfDouble



受け入れることを期待するかもしれませんがDoubleToTheInt



引数を他の型などにDoubleToTheInt



ます。 しかし、それは起こっていることではありません。







試してください-両方の行で同じ結果が表示されます。







 emiller@gibbon ~> clang something.c emiller@gibbon ~> ./a.out (0.99)^100: 0.366032 (0.99)^100: 0.366032
      
      





すべてをint



からfloat



変更してみてくださいFloatPowerOfDouble



がさらにFloatPowerOfDouble



動作をすることがFloatPowerOfDouble



ます。 はい







 double DoubleToTheFloat(double base, float power) { return pow(base, power); } int main() { double (*FloatPowerOfDouble)(float, double) = (double (*)(float, double))&DoubleToTheFloat; printf("(0.99)^100: %lf \n", DoubleToTheFloat(0.99, 100)); // OK printf("(0.99)^100: %lf \n", FloatPowerOfDouble(100, 0.99)); // ... }
      
      





生産する:







 (0.99)^100: 0.366032 (0.99)^100: 0.000000
      
      





2行目の値は「エラーでもない」-問題が引数の順列にある場合、答えは0ではなく100 ^ 0.99 = 95.5になると予想されます。







上記のコード例は、関数の句読点です。危険な形式の「アセンブラーのないアセンブラー」で、職場、重機の隣、または処方薬と組み合わせて使用​​しないでください。 これらの例は、アセンブラレベルでコードを理解している人には絶対に理にかなっていますが、他の人を混乱させる可能性があります。







私は少しcheしました-64ビットのx86コンピューターでコードを実行することをお勧めします。 別のアーキテクチャでは、このトリックが機能しない場合があります。 Cには無限の数のダークアングルがあると考えられていますが、int引数とdouble引数の動作は、C標準の一部ではありません。これは、最新のx86マシンで関数が呼び出される方法の結果であり、気の利いたプログラミングトリックに使用できます。







これは私の署名ではありません。



大学でCを勉強した場合、引数がスタック上の関数に渡されることを覚えているかもしれません。 呼び出し元は引数をスタックに逆順でプッシュし、関数はスタックから引数を読み取ります。







少なくとも彼らは私にそれをそのように説明しましたが、今日のほとんどのコンピューターは最初のいくつかの引数を直接CPUレジスターに渡します。 したがって、関数をスタックから読み取る必要はありません。これは、レジスタよりもはるかに低速です。







関数の引数に使用されるレジスタの数と配置は、呼び出し規約によって異なります。 Windowsには1つの規則があります -浮動小数点値用の4つのレジスターとポインターと整数用の4つのレジスター。 Unixには、 System V規約と呼ばれる別の規約があります 。 浮動小数点引数用に8つのレジスタがあり、ポインターと整数用にさらに6つあります。 (引数がレジスタに収まらない場合、それらは古いスタックで送信されます。)







Cでは、ヘッダーファイルは、多くの場合レジスタとスタックを組み合わせて、関数の引数を配置する場所をコンパイラに伝えるためにのみ存在します。 各呼び出し規約には、これらの引数をレジスタおよびスタックに配置するための独自のアルゴリズムがあります。 たとえば、Unixは構造を破壊し、すべてのフィールドをレジスタに収めようとするのに非常に積極的ですが、Windowsは少し怠け者で、大きなパラメータ構造にポインタを渡すだけです。







しかし、WindowsとUnixの両方で、基本的なアルゴリズムは次のように機能します。









DoubleToTheInt



関数への引数がどのようにDoubleToTheInt



れるかを見てみましょう。







関数のシグネチャは次のとおりです。







  double DoubleToTheInt(double base, int power);
      
      





コンパイラがDoubleToTheInt(0.99, 100)



遭遇すると、コンパイラはDoubleToTheInt(0.99, 100)



ようにレジスタをDoubleToTheInt(0.99, 100)



ます。







RDX RCX R8 R9
100 ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 ??? ??? ???


(簡単にするために、Windows呼び出し規約を使用します。)そのような関数が返された場合:







  double DoubleToTheDouble(double base, double power);
      
      





引数は次のように配置されます。







RDX RCX R8 R9
??? ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 100 ??? ???


これで、記事の最初から少し焦点が合っている理由を推測できたかもしれません。 次の関数シグネチャを検討してください。







  double IntPowerOfDouble(int y, double x);
      
      





IntPowerOfDouble(100, 0.99)



呼び出すことにより、コンパイラは次のようにレジスタを配置します。







RDX RCX R8 R9
100 ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 ??? ??? ???


つまり、 DoubleToTheInt(0.99, 100)



とまったく同じです!

コンパイルされた関数は、どのように呼び出されたのかわからないため、レジスタ内およびスタックのどこで引数を期待するかのみ- 関数ポインタを誤った(ただしABI互換の)関数シグネチャにキャストすることで、引数の順序を変えて関数を呼び出すことができます







実際、整数引数と浮動小数点引数が順序を維持している限り、必要に応じてそれらをシャッフルでき、レジスタの配置は同じになります。 つまり、







double functionA(double a, double b, float c, int x, int y, int z);









以下と同じレジスタ配置になります。







double functionB(int x, double a, int y, double b, int z, float c);









と同じ:







double functionC(int x, int y, int z, double a, double b, float c);









3つの場合すべてで、レジスタは次のようになります。







RDX RCX R8 R9
int x



int y



int z



???
XMM0 XMM1 XMM2 XMM3
double a



double b



double c



???


倍精度引数と単精度引数の両方がXMMレジスタを占有することに注意してください-ただし、これらは互いにABI互換ではありません 。 したがって、最初の2番目のコード例を覚えている場合、 FloatPowerOfDouble



がゼロ(95.5ではなく)を返した理由は、コンパイラがXMM0に単精度値(32ビット)100.0を配置し、倍精度値(64ビット) XMM1では0.99-ただし、呼び出される関数は、XMM0では倍精度の数値、XMM1では単一の数値を予期していました。 このため、指数は仮数のふりをし、仮数ビットは切り捨てられるか、指数と間違えられ、 FloatPowerOfDouble



関数は非常に小さい数を非常に大きい数のべき乗してゼロになりました。 謎は解決されました。







に注意してください??? 上記の表で。 これらのレジスタの値は定義されていません-以前の計算からの任意の値が可能です。 呼び出される関数は、それらに何があっても関係なく、実行時にそれらを上書きできます。







これにより、興味深い機会が生まれます。引数の順序が異なる関数を呼び出すことに加えて、引数の数が異なる関数を呼び出すこともできます 。 そんなにクレイジーなことをしたい理由はいくつかあります。







1-800-I-Really-Enjoy-Type-Punningをダイヤルします



これを試してください:







 #include <math.h> #include <stdio.h> double DoubleToTheInt(double x, int y) { return pow(x, y); } int main() { double (*DoubleToTheIntVerbose)( double, double, double, double, int, int, int, int) = (double (*)(double, double, double, double, int, int, int, int))&DoubleToTheInt; printf("(0.99)^100: %lf \n", DoubleToTheIntVerbose( 0.99, 0.0, 0.0, 0.0, 100, 0, 0, 0)); printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100)); }
      
      





両方の行で同じ結果が得られることは驚くことではありません。すべての引数はレジスタに配置され、レジスタの位置は同じです。







今から楽しみが始まります。 引数がレジスタに挿入され、関数が同じタイプを返す場合、多くの異なるタイプの関数を呼び出すことができる新しい「冗長」タイプの関数を定義できます。







 #include <math.h> #include <stdio.h> typedef double (*verbose_func_t)(double, double, double, double, int, int, int, int); int main() { verbose_func_t verboseSin = (verbose_func_t)&sin; verbose_func_t verboseCos = (verbose_func_t)&cos; verbose_func_t verbosePow = (verbose_func_t)&pow; verbose_func_t verboseLDExp = (verbose_func_t)&ldexp; printf("Sin(0.5) = %lf\n", verboseSin(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0)); printf("Cos(0.5) = %lf\n", verboseCos(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0)); printf("Pow(0.99, 100) = %lf\n", verbosePow(0.99, 100.0, 0.0, 0.0, 0, 0, 0, 0)); printf("0.99 * 2^12 = %lf\n", verboseLDExp(0.99, 0.0, 0.0, 0.0, 12, 0, 0, 0)); }
      
      





このような型の互換性は、たとえば、倍精度の数値を受け入れて返す関数を参照する単純な計算機を作成できるため便利です。







 #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> typedef double (*four_arg_func_t)(double, double, double, double); int main(int argc, char **argv) { four_arg_func_t verboseFunction = NULL; if (strcmp(argv[1], "sin") == 0) { verboseFunction = (four_arg_func_t)&sin; } else if (strcmp(argv[1], "cos") == 0) { verboseFunction = (four_arg_func_t)&cos; } else if (strcmp(argv[1], "pow") == 0) { verboseFunction = (four_arg_func_t)&pow; } else { return 1; } double xmm[4]; int i; for (i=2; i<argc; i++) { xmm[i-2] = strtod(argv[i], NULL); } printf("%lf\n", verboseFunction(xmm[0], xmm[1], xmm[2], xmm[3])); return 0; }
      
      





私たちはチェックします:







 emiller@gibbon ~> clang calc.c emiller@gibbon ~> ./a.out pow 0.99 100 0.366032 emiller@gibbon ~> ./a.out sin 0.5 0.479426 emiller@gibbon ~> ./a.out cos 0.5 0.877583
      
      





Mathematicaの競争相手ではありませんが、関数名と対応する関数ポインタのテーブルを備えたより複雑なバージョンを想像できます-新しい関数を追加するには、テーブルを更新し、コードで明示的に新しい関数を呼び出さなくても十分です。







その他の用途には、JITコンパイラが含まれます。 LLVMチュートリアルを学習したことがある場合、思いがけずメッセージが表示される場合があります。







「フル機能の引数の受け渡しはまだサポートされていません!」


LLVMはコードを巧みにマシンコードに変換し、マシンコードをメモリにロードしますが、メモリにロードされた関数を呼び出す必要がある場合、柔軟性はあまりありません。 LLVMRunFunction



を使用すると、 main()



ような関数(整数引数、ポインター引数、ポインター引数、整数を返すmain()



を呼び出すことができますが、それ以上のLLVMRunFunction



はできません。 ほとんどのチュートリアルでは、コンパイラー関数をmain()



似た関数でラップし、すべての引数をポインター引数の後ろに隠し、ラッパーを使用してポインターから引数を引き出して実際の関数を呼び出すことをお勧めします。







しかし、X86レジスタに関する新しい知識があれば、多くの場合、ラッパー関数を取り除くことで式を簡素化できます。 関数がC呼び出し可能な関数シグネチャの限定リスト( int main()



int main(int)



int main(int, void *)



など)に属していることを確認する代わりに、ポインター、シグネチャを作成できますこれは、すべてのパラメータレジスタを埋めるため、レジスタを介してのみ引数を渡し、それらを呼び出すすべての関数と互換性があり、未使用の引数にはゼロ(または何でも)を渡します。 可能なすべての関数シグネチャではなく、戻り値の型ごとに個別の型を定義する必要があり、そうでなければアセンブラの使用を必要とするメソッドを使用してより柔軟に関数を呼び出します。







店を閉める前に最後のトリックを紹介します。 このコードがどのように機能するかを理解してください:







 double NoOp(double a) { return a; } int main() { double (*ReturnLastReturnValue)() = (double (*)())&NoOp; double value = pow(0.99, 100.0); double other_value = ReturnLastReturnValue(); printf("Value: %lf Other value: %lf\n" value, other_value); }
      
      





(最初に呼び出し規約を読む必要があります...)







翻訳理論

関数は、XMM0を介して結果を返します。 2つの関数の間では何も起こりません。XMM0では、最後の関数の結果が残り、 NoOp



が引数として選択して戻ります。







少しアセンブラーがかかります



プログラマーのフォーラムでアセンブラーに尋ねた場合、通常の答えは次のとおりです。アセンブラーは必要ありません。コンパイラーを書く優秀な科学者に任せてください。 はい、手を覗いてください。







コンパイラライターは賢い人ですが、他の人はアセンブラを慎重に避けるべきだと考えるのは間違いだと思います。 タイピングのちょっとした試みで、レジスタのレイアウトと呼び出し規則(おそらくアセンブラーコンパイラライターの排他的な関心事)がCでときどき現れること、そしてこの知識を使って普通のCプログラマーが不可能だと思うことをする方法を見ました。







しかし、これはアセンブラープログラミングの氷山の一角にすぎません-特に1行のアセンブラーコードなしで表示されます-そして、このトピックをさらに深く掘り下げて時間を割くすべての人にアドバイスします。 アセンブラーは、CPUが命令を実行する方法(命令カウンターとは何か、フレームポインターとは何か、スタックポインターとは何か、レジスターは何か)を理解するための鍵であり、異なる(より明るい)プログラムを見ることができます。 基本的な知識さえあれば、思いもよらなかった解決策を思い付くことができ、お気に入りの高級言語の刑務所をすり抜けて、厳しい美しい太陽の下で目を細めると何が起こるかを理解できました。








All Articles