円周を計算する

「円の直径を浮動小数点数として受け取り、円の長さを浮動小数点数として返す関数をC ++で記述してください。」



C ++コースの最初の週のタスクのように聞こえます。 しかし、これは一見しただけです。 問題解決の最初の段階ですでに問題が発生しています。 いくつかのアプローチを検討することを提案します。



学生:このオプションはどうですか?



#include <math.h> float CalcCircumference1(float d) { return d * M_PI; }
      
      





先生:はい、このコードは正常にコンパイルできます。 またはそうでないかもしれません。 M_PIは、CまたはC ++標準では定義されていません。 これはVC ++ 2005のコンパイラで動作しますが、それ以降のバージョンでは、math.hを有効にしてこの非標準定数にアクセスする前に#define _USE_MATH_DEFINESを使用する必要があります。 その結果、他のコンパイラでは処理できないコードを作成します。



シーン2



学生:あなたの知恵をありがとう、先生。 非標準の定数M_PIへの依存関係を削除しました。 それは良いですか?



 float CalcCircumference2(float d) { return d * 3.14159265358979323846; }
      
      





先生:はい、そうです。 このコードがコンパイルされ、目的の結果が得られます。 ただし、コードは非効率的です。 単精度の数値に倍精度の定数を掛けます。 コンパイラは、float型の関数パラメーターをdouble型にキャストしてから、逆変換を実行して戻り値を取得する必要があります。 SSE2のコードをコンパイルすると、依存関係チェーンに2つの命令が追加され、計算に3倍の時間がかかります! ほとんどの場合、このような遅延は非常に許容されますが、内部サイクルでは悪影響が非常に大きくなる可能性があります。



x87プラットフォーム用にコンパイルする場合、double型への変換には費用はかかりませんが、逆変換には費用がかかります-高価なため、一部の最適化コンパイラはこの変換を破棄し、その結果、CalcCircumference(r)== CalcCircumference(r)はfalseを返します!



シーン3



学生:先生、ありがとう。 正直なところ、SSE2とx87が何であるかはわかりませんが、型が一貫しているとコードがどれほどエレガントになるかわかります。 これは本当の詩です。 単精度定数を使用します。 いかがですか?



 float CalcCircumference3(float d) { return d * 3.14159265358979323846f; }
      
      





先生:はい、素晴らしい! 定数の最後にある記号「f」はすべてを変更します。 生成されたマシンコードを見ると、このオプションがはるかにコンパクトで効率的であることを理解できます。 ただし、スタイルについてはコメントがあります。 この不思議な定数は関数内に場所がないと思いませんか? これがPi番号であり、その値が変更される可能性が低い場合でも、定数に名前を付けてヘッダーファイルに配置することをお勧めします。



シーン4



学生:ありがとう。 すべてを非常にわかりやすく説明します。 以下のコード行を一般的なヘッダーファイルに配置し、関数で使用します。 大丈夫ですか?



 const float pi = 3.14159265358979323846f;
      
      





先生:はい、素晴らしい! 「const」キーワードを使用して、変数を変更してはならず、変更できないことを示しました。さらに、ヘッダーファイルに変数を配置できるようになりました。 しかし、残念ながら、C ++でスコープを定義する際の微妙な点を掘り下げなければなりません。



constキーワードでpiを宣言すると、静的キーワード効果がボーナスとして得られます。 整数型の場合、これは正常ですが、異なるデータ型(浮動小数点数、配列、クラス、構造)を処理している場合、ヘッダーファイルを含む各翻訳単位で変数のメモリを個別に割り当てることができます。 場合によっては、float型の変数のインスタンスが数十または数百に達することがあり、実行可能ファイルは不当に大きくなります。



シーン5



学生:冗談ですか? そして何をすべきか?



先生:はい、理想からはほど遠いです。 定数宣言に__declspec(selectany)または__attribute __(weak)属性を配置して、VC ++およびGCCがそれぞれ、この定数の多くのコピーの1つを保存するのに十分であることを理解できるようにします。 しかし、あなたと私は科学の理想主義的な世界にいるので、標準のC ++構成体を使用することを主張します。



シーン6



学生:こんな感じですか? C ++ 11からconstexprを使用していますか?



 constexpr float pi = 3.14159265358979323846f;
      
      





先生:はい。 これでコードは完璧です。 もちろん、VS 2013はconstexprをどうするかわからないため、コンパイルできません。 ただし、Visual C ++コンパイラ2013年11月CTPツールキットまたはGCCまたはClangの最新バージョンをいつでも使用できます。



学生:#defineを使用できますか?



先生:いいえ!



学生:ああ、これすべてで地獄に! 私はバリスタなりました



シーン7



学生:やめて、何か覚えています。 とても簡単です! コードは次のようになります。



 mymath.h: extern const float pi; mymath.cpp: extern const float pi = 3.14159265358979323846f;
      
      





先生:まさに、ほとんどの場合、それは正しい決定です。 しかし、DLLで作業している場合、外部関数はDLLのmymath.hにどのようにアクセスしますか? この場合、このシンボルのエクスポートとインポートを提供する必要があります。



問題は、整数型の規則が完全に異なることです。 C ++ヘッダーファイルに以下を追加することをお勧めします。



 const int pi_i = 3;
      
      





Pi番号は十分に正確に指定されていませんが、実際には、ヘッダーファイル内の整数定数は他の定数とは異なり、メモリ割り当てを必要としません。 この違いの理由は完全には明らかではありませんが、ほとんどの場合、これは重要ではありません。



数年前、重要なDLLの1つが突然2 MB増加した理由を調べるように求められたときに、「静的」が「定数」の意味を見つけました。 ヘッダーファイルに定数の配列があり、DLLにこの配列のコピーが30個あることがわかりました。 つまり、それは時々重要です。



もちろん、この場合、#defineは恐ろしい選択だと思います。 たぶんこれは最悪の解決策ではありませんが、私はそれがまったく好きではありません。 #defineを使用したpi宣言が原因でコンパイルエラーが発生したことがあります。 十分ではありません、私はあなたに言います! 名前空間をポイ捨てすることが、#defineをできるだけ避けるべき主な理由です。



おわりに



これらすべてから学んだ教訓は正確にはわかりません。 ヘッダーファイルや構造体、または定数の配列でfloatやdoubleなどの定数を宣言するときに生じる問題の本質は、誰にも明らかではありません。 ほとんどの深刻なプログラムでは、このために静的定数の重複が発生し、場合によっては不当に大きくなります。 constexprはこの問題から私たちを救うことができると思いますが、確かに知るためにそれを使用する十分な経験がありません。



「実際の」サイズよりも数百キロバイト大きいプログラムに出くわしましたが、これはすべてヘッダーファイルの定数の配列が原因でした。 また、このクラスオブジェクトはヘッダーファイルでconst型として定義されていたため、最終的にクラスオブジェクトのコピーが50個(さらにコンストラクターとデストラクターへの呼び出しが50個)発生するプログラムを見ました。 つまり、考えるべきことがあります。



ここからテストプログラムダウンロードすると、GCCでこれがどのように起こるかを確認できます 。 makeコマンドでビルドしてから、objdump -d constfloat | grep fldsを使用して、データセグメント内の隣接アドレスから4つの読み取り命令を見つけます。 さらにスペースを取りたい場合は、以下をheader.hに追加します。



 const float sinTable[1024] = { 0.0, 0.1, };
      
      





GCCの場合、増加は変換レコード(ソースファイル)ごとに4 KBになります。つまり、誰もテーブルに一度もアクセスしない場合でも、実行可能ファイルは20 KiBずつ増加します。



いつものように、浮動小数点数を使用した演算はかなりの困難に関連付けられていますが、この場合、C ++言語のあまりにも遅い進化は責任があると思われます。



このトピックについて他に読むべきこと:



VC ++:重複を回避する方法と、重複を回避できないことを理解する方法



VC ++ 2013 Update 2のコンパイラーは、/ Gwオプションを導入しました。これは、各グローバル変数を個別のCOMDATコンテナーに配置し、リンカーが重複を識別して取り除くことを可能にします。 このアプローチは、ヘッダーファイルで定数と静的変数を宣言することによる悪影響を避けるのに役立つ場合があります。 Chromeでは、このような変更により約600 KB( 詳細 )を節約できました。 twoPiDoubleとpiDouble(およびtwoPiFloatとpiFloat)の数千のインスタンスを削除することで、この節約の一部が達成されました(驚き!)。



ただし、VC ++ 2013 STLでは、/ Gwが削除できないクラス宣言でstaticまたはconstとして宣言されたオブジェクトがいくつかあります。 これらのオブジェクトはすべて1バイトを占有しますが、最終的には45キロバイト以上が実行されます。 開発者にこのエラーについて通知し、 VC ++ 2015で修正されたという応答を受け取りました。



著者のリクエストにより、オリジナルへのリンクをここで共有します



All Articles