VS2019のVC++で追加されたvcruntime140_1.dll ♯2

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で確認します。

Dependency Walkerによるvcruntime140_1.dllのエクスポート関数の確認

エクスポート関数としては、以下の三つが定義されています。

  • __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 への参照は発生していません。

FH4を有効化時の参照(try/catchを不使用)

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を有効化時の参照(try/catchを使用)

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関数への参照も発生していません。

FH4を無効化時の参照(try/catchを不使用)

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を無効化時の参照(try/catchを使用)

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を無効化したときの期待した動作ではありません。

FH4を無効化時の参照(try/catch/nothrowを使用)

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を無効化時の参照(try/catch/nothrow/local定義を使用)

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ファイルへの参照を発生させないための具体的な方法に関する投稿でした。

コメントを残す