Visual Studio 2019からVC++のランタイムライブラリファイルの一つとして追加されたvcruntime140_1.dllに関する投稿です。
Visual Studio 2019で追加されたvcruntime140_1.dll
今回の投稿では、Visual Studio 2019やVisual Studio 2022で追加された「vcruntime140_1.dll
を必要とする機能となどんなものなのか ? 」、また、「その機能を無効化できるのか?」 を探っていきます。
エクスポートされている関数
まずは、vcruntime140_1.dll
(Ver. 16.3)がエクスポートしている関数をdependency walkerで確認します。
エクスポート関数としては、以下の三つが定義されています。
__CxxFrameHandler4
__NLG_Dispatch2
__NLG_Return2
後者の二つは、マイクロソフトのドキュメントにも記載があるように内部実装用のCRTの関数です。__CxxFrameHandler4
が、Visual Studio 2019のVC++で追加された機能を実現するものです。
例外処理に対する新機能
このvcruntime140_1.dll
を導入することになった機能の説明が、マイクロソフトのC++チームのブログにあります。
Making C++ Exception Handling Smaller On x64 (英語) (x64におけるC++例外処理のバイナリサイズを縮小する)
このブログの冒頭を引用します。
Visual Studio 2019 Preview 3 introduces a new feature to reduce the binary size of C++ exception handling (try/catch and automatic destructors) on x64. Dubbed FH4 (for __CxxFrameHandler4, see below), I developed new formatting and processing for data used for C++ exception handling that is ~60% smaller than the existing implementation resulting in overall binary reduction of up to 20% for programs with heavy usage of C++ exception handling.
これを日本語訳します。
Visual Studio 2019 Preview 3では、x64でのC++例外処理(try / catch、自動デストラクタ)のバイナリサイズを縮小する新機能が導入されました。FH4(__CxxFrameHandler4の略、後述)と名付けられ、C++例外処理に使用するデータのフォーマットと処理を新たに開発し、既存の実装よりも〜60%小さくなった結果、C++例外処理を多用するプログラムでは全体で最大20%のバイナリサイズの削減が可能になりました。
コンパイラーが作成するC++の例外処理(try/catch)のコードサイズを小さくする新たな仕組み(FH4)を導入し、その処理でvcruntime140_1.dll
がエクスポートしている関数の __CxxFrameHandler4
が使われているようです。
FH4の有効化と既定値
例外処理を多用したプログラムのバイナリーサイズを小さくする新機能であるFH4ですが、Visual Studio 2019の当初は既定では有効化されていませんでした。有効化するにはコンパイラーオプションで /d2FH4
を設定する必要がありました。
既定で有効化されなかった理由は、
- Microsoftストアアプリ向けの対応が間に合っていない
です。その後、以下の問題も見つかりました。
- デバック実行時用のフックが欠落していた問題
結果として、既定で有効化されたのは、Visual Studio 2019 Update 3 (16.3) です。
そのため、例外処理を使用したプログラムでは、 Visual Studio 2019 Update 3 (16.3) 以降で既定のコンパイラー設定でビルドすると、vcruntime140_1.dll
への参照が発生することになります。
Visual Studio 2019 以降でのC++ビルドでvcruntime140_1.dllをリンクされなくする方法
Visual Studio 2019で追加された機能である例外処理の新しい仕組みはFH4です。それまでの仕組みはFH3となります。
新しい例外処理のFH4では、その実現のためにvcruntime140_1.dll
がエクスポートしている関数の __CxxFrameHandler4
が使われます。
従来の例外処理のFH3では、その実現のためにvcruntime140.dll
がエクスポートしている関数の __CxxFrameHandler3
が使われます。
そのため、(Visual Studio 2022以降も含む)Visual Studio 2019 Update 3 (16.3)以降であっても従来のFH3を利用すれば、vcruntime140_1.dll
のファイルは必要なくなります。
Visual Studio 2019 Update 3 (16.3)以降でFH3を使う設定
では、従来のFH3を使う設定はどのようにすればよいのでしょうか?
これも、マイクロソフトのC++ブログにありました。ブログの主記事ではなく、コメント部分にあります。
VS2019 -> Properties -> C/C++ -> Command Line add ‘/d2FH4-‘ VS2019 -> Properties -> Linker -> Command Line add ‘/d2:-FH4-‘
既定値がFH3であった時に、FH4を有効化するオプションは /d2FH4
でした。無効化するためには -
をつければよいです。この部分は他のオプションの規則と同じですね。
マイクロソフトのブログでは、VS2019の記載しかありませんが、VS2022でも同じオプションが使えます。
FH4をコンパイラーオプションで無効化しても vcruntime140_1.dll を参照することがある
前節で説明したFH4を無効化するオプションをコンパイラーとリンカーに設定すれば、ほとんどの場合は、vcruntime140_1.dll
への参照はなくなります。しかし、特定のパターンのコードがあると、vcruntime140_1.dll
への参照が残ることがわかりました。
FH4が有効の場合
try/catchを使っていない場合
FH4が有効であっても、プログラムが例外処理を使っていなければ、vcruntime140_1.dll
への参照は発生しません。
#include <new>
int main()
{
char* pBuffer = nullptr;
//try {
pBuffer = new char[10];
//}
//catch (...) { }
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
への参照は発生していません。
try/catchを使っている場合
FH4が有効、かつ、プログラムが例外処理を使っていると、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照が発生します。
#include <new>
int main()
{
char* pBuffer = nullptr;
try {
pBuffer = new char[10];
}
catch (...) { }
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照が発生しています。
FH4が無効の場合 (期待した動作)
コンパイラーオプションの /d2FH4- を使って、FH4を無効化した場合の vcruntime140_1.dll
への参照の状態を確認します。期待する動作としては参照が発生しないことです。
try/catchを使っていない場合
FH4が無効の場合、プログラムが例外処理を使っていなければ、FH4が有効のときと同様に、vcruntime140_1.dll
への参照は発生しません。
#include <new>
int main()
{
char* pBuffer = nullptr;
//try {
pBuffer = new char[10];
//}
//catch (...) { }
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
への参照は発生していません。try/catchを使っていないため、 vcruntime140.dll
内の __CxxFrameHandler3
関数への参照も発生していません。
try/catchを使っている場合
FH4が無効、かつ、プログラムが例外処理を使っていると、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照は発生せず、 vcruntime140_1.dll
内の__CxxFrameHandler3
関数への参照が発生します。
#include <new>
int main()
{
char* pBuffer = nullptr;
try {
pBuffer = new char[10];
}
catch (...) { }
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照は発生しておらず、代わりにvcruntime140.dll
内の__CxxFrameHandler3
関数への参照 が発生しています。
FH4が無効の場合 (期待と異なる動作)
コンパイラーオプションの /d2FH4- を使って、FH4を無効化した場合の期待する動作としてはvcruntime140_1.dll
への参照が発生しないことです。しかし、参照が発生するパターンがあることがわかりました。
nothrowを使っている場合
C++では、new
の処理で例外を発生させない方法として、new(std::notrhow)
があります。
この new(std::notrhow)
をコード中で使っていると、FH4を無効にした場合でも、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照が発生することがわかりました。
#include <new>
int main()
{
char* pBuffer = nullptr;
try {
pBuffer = new(std::nothrow) char[10];
}
catch (...) { }
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照が発生しています。これは、FH4を無効化したときの期待した動作ではありません。
nothrowを使った場合に vcruntime140_1.dll への参照を発生させない対策
vcruntime140_1.dllへの参照を発生するのを避けるためにFH4を無効化するのに、引き続き vcruntime140_1.dll への参照が発生してしまうのでは、意味がありません。対策を考えます。
いろいろ試したところ二つの対策が見つかりました。
- nothrowの使用をやめる
- nothrowを引数に持つnewオペレータ-をローカルで定義する
です。
nothrowの使用をやめる
通常のnew
は、newの処理中にエラーが発生したら例外が発生します。しかし、new(std::nothrow)
は、newの処理中にエラーが発生したときに例外を発生させず、エラーが発生した場合はnullptr
を返します。
参照が発生している原因がnothrow
を使っていることのため、nothrow
を使ったコードをnothrow
を使わないコードに書き換えることで、 vcruntime140_1.dll を参照してしまう問題を回避することができます。
具体的な方法としては以下の通りです。
char* pBuffer = new(std::nothrow) char[10];
上記のようにnotrhowを使ったコードは、下記のようなnothrowを使わない等価のコードに書き直すことができます。
char* pBuffer = nullptr;
try {
pBuffer = new char[10];
} catch (...) {}
このようにコードを修正すれば、nothrowを使わなくなるため問題を回避できます。しかし、nothrow
がいたるところで大量に使われている場合、コードの書き換えは大変です。
大量のコードの書き換えを回避したい場合は、この方法は使えません。
nothrowを引数に持つnewオペレータ-をローカルで定義する
二つ目の方法は、 nothrowを引数にもつnewオペレーターをローカルで定義する方法です。
コード上でnothrowを使っていると、 vcruntime140_1.dll への参照が発生してしまうのは、標準で用意されている nothrowを引数にもつnewオペレーターの実装が、FH4を無効化したときにFH4を使わない実装に置き換わらないために発生しているようです。
これが原因であれば、VC++が標準で用意している nothrowを引数にもつnewオペレーターの実装を利用しなければ問題が発生しません。 VC++の標準の実装を使わなくするために、ローカルで nothrowを引数にもつnewオペレーターを実装します。
具体的には以下のコードをプログラムのどこかに定義・実装します。
_NODISCARD _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(size_t const _Size, ::std::nothrow_t const&) noexcept
{
try
{
return operator new(_Size);
}
catch (...)
{
return nullptr;
}
}
すると、このプログラム側で実装したnewオペレーターが使われるようになります。
これにより、 nothrowを使ったコードで vcruntime140_1.dll を参照してしまう問題を回避することができます。
nothrowを使いつつnewオペレータをローカルで定義した場合
実際に上記の対策が有効であるかどうかを確認しました。
#include <new>
_NODISCARD _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(size_t const _Size, ::std::nothrow_t const&) noexcept
{
try
{
return operator new(_Size);
}
catch (...)
{
return nullptr;
}
}
int main()
{
char* pBuffer = nullptr;
try {
pBuffer = new(std::nothrow) char[10];
}
catch (...) {
}
if (pBuffer) delete[] pBuffer;
}
実際に上記のコードを使用してDependency Walkerで確認しました。下記のDependency Walkerの結果のとおり、vcruntime140_1.dll
内の__CxxFrameHandler4
関数への参照は発生しておらず、代わりにvcruntime140.dll
内の__CxxFrameHandler3
関数への参照 が発生しています。
FH4無効化のバグと思われるnothrow問題の回避策となっていることがわかります。
FH4を無効化して vcruntime140_1.dll への参照を発生させない方法のまとめ
Visual Studio 2019 Update 3以降、および、Visual Studio 2022以降を使ってビルドするアプリにおいて、FH4を無効化して vcruntime140_1.dll への参照を発生させない方法のまとめです。
コンパイラーオプションでFH4を無効化する
C++プロジェクトのプロパティで以下の設定をします。
C++ Project -> Properties -> C/C++ -> Command Line add ‘/d2FH4-‘ C++ Project -> Properties -> Linker -> Command Line add ‘/d2:-FH4-‘
なお、マイクロソフトのC++チームのブログでは、FH4を無効化するためには、上記のようにコンパイラーオプションとリンカーオプションの両方を設定することになっていました。しかし、実際に試してみると、Visual Studioを使ってビルドしている場合は、前者の設定のみでもFH4を無効化できるようです。
コード上でnothrowを使っている場合に必要な追加の対策
コード上でnothrow
を使っている場合は、標準で用意されている nothrowを引数にもつnewオペレーターの実装を置き換える必要があります。
そのために以下のコードをプログラムのどこかに定義・実装します。
_NODISCARD _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(size_t const _Size, ::std::nothrow_t const&) noexcept
{
try
{
return operator new(_Size);
}
catch (...)
{
return nullptr;
}
}
以上、今回は、Visual Studio 2019から追加された vcruntime140_1.dll
ファイルへの参照を発生させないための具体的な方法に関する投稿でした。