AviUtlの6年半ぶりの新バージョン、1.10が正式に公開されました。このバージョン(正確には1.10rc1)から、AviUtlは32bitアプリでありながら4GB以上のメモリを使えるようになりました。

これの何がすごいのか、どのようにすれば実現できるのかを説明します。

注: 共有メモリを利用した32bitアプリの4GBの壁を超える方法の一例を紹介します。 AviUtlをリバースエンジニアリングしたわけではないので、実際の実装方法とは異なる可能性があります。

4GBの壁について

32bitアプリには4GBの壁と呼ばれる使用可能メモリの上限値があります1。そもそも32bitアプリの32bitとは、メモリアドレスを32bitで表現していることに由来しています。 メモリアドレスは1byte毎に割り当てられているため、32bitのアドレスで表せる上限は4GBであり、これを超える量のメモリは使いたくても使えません。 この制限のため、どんなにPCに大量のメモリを積んでいたとしても、OSが64bitだったとしても、32bitアプリである限り、1プロセスが普通に使えるメモリは4GBを超えることはありえません。

ところが今回AviUtlは、32bitアプリでは絶対に逃れられないはずの4GBの壁を32bitアプリのまま乗り越えました。

余談: PAE(物理アドレス拡張)について OSは32bitであってもPAEというCPUの機能を使うことで物理メモリのアドレスを36bitに拡張することができ、64GBまでメモリを認識・利用することが可能です。ただし、アプリケーションの仮想メモリのアドレスは32bitままなので4GBの壁は依然として残ります。

AviUtlが64bitアプリにならない理由

近年ではカメラやディスプレイの高解像度化により、動画編集でもより多くのメモリを使いたい状況になっています。 64bitアプリであればメモリアドレスを64bitで表すため、理論上は16EB(エクサバイト)という巨大なメモリ空間を扱えます。

しかし、AviUtlのプラグインは32bitのDLLとして提供されているため、本体を64bitにすると多くの有用なプラグインがすべて利用できなくなってしまいます。 このため、32bitアプリのまま4GBの壁を乗り越えてより多くのメモリを利用する方法が実装されました。

以下にKENくん氏のツイートを引用します。

これらのツイートにもあるように、AviUtlでは__共有メモリ__を利用することで4GB以上のメモリを利用しています。

Windowsの共有メモリの使い方

Windowsで共有メモリを利用するにはCreateFileMappingMapViewOfFileというAPIを利用します。

HANDLE CreateFileMapping(
  HANDLE                hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD                 flProtect,
  DWORD                 dwMaximumSizeHigh,
  DWORD                 dwMaximumSizeLow,
  LPCSTR                lpName
);
LPVOID MapViewOfFile(
  HANDLE hFileMappingObject,
  DWORD  dwDesiredAccess,
  DWORD  dwFileOffsetHigh,
  DWORD  dwFileOffsetLow,
  SIZE_T dwNumberOfBytesToMap
);

「共有メモリ」という名前の通り、元々は複数のプロセスの間で同じメモリ領域を共有する仕組みです。 また、「File」という名前が付いていますが、hFile引数にINVALID_FILE_HANDLEを指定することで、特定のファイルには紐付かないメモリ領域を作って利用することができます。

詳しくはMicrosoftのドキュメント「Creating Named Shared Memory」を参照してください。

CreateFileMapping ― 共有メモリ領域の作成

CreateFileMappingで確保する領域のサイズ上限は、dwMaximumSizeHighdwMaximumSizeLowの2つの32bitの引数を使って64bitで指定します。この時点ですでに4GBを超えるサイズを指定できることがわかります。

また、このAPIを呼び出しただけではプロセスの仮想メモリにメモリ領域が確保されるわけではなく、OSの管理下に確保される2だけなので、32bitアプリの4GBの壁に引っかかりません。

MapViewOfFile ― 共有メモリ領域の利用

CreateFileMappingで確保した領域をプロセスから使うには、MapViewOfFile関数で共有メモリ領域をプロセスの仮想メモリ空間にマッピングします。 仮想メモリ空間には4GBの壁があるため、4GBを超えるようなサイズを一度にマッピングすることはできません。 そのため、オフセットとサイズを指定して共有メモリ領域の一部をプロセスの仮想メモリ空間にマッピングすることになります。

サイズを指定する引数dwNumberOfBytesToMapは32bitアプリでは32bit値なので、4GBを超える指定はできません。 一方、オフセットはdwFileOffsetHighdwFileOffsetLowの2つの32bit値を使って、64bitで指定します。 これによって共有メモリ領域の4GBを超えた位置を指定してマッピングすることができます。

一度にアクセスできるのはこのマッピングした領域だけですが、この領域をUnmapViewOfFile APIで解放したうえで、別の領域をマッピングしなおすことで、実質的に4GB以上のメモリを利用できます。

これが、仮想メモリを利用して4GB以上のメモリを利用する方法、すなわち4GBの壁を超える方法になります。

デモ

32bitアプリで4GB以上のメモリを利用するデモプログラムを用意しました。

https://github.com/makiuchi-d/win32-memory-test/blob/master/filemap.c

CreateFileMappingで8GBの領域を作成し、前半のループでは0.25GBずつをMapViewOfFileでマッピングしながら6GB分の領域に値を書き込みます。 後半のループでは書き込まれた値が保存されていることを確認しています。

このプログラムを32bitコンソールアプリとしてビルドして実行してみてください。 タスクマネージャでメモリ使用量を見ると、プロセス自体の使用メモリは4GBを超えることはありませんが、システム全体のメモリ使用量は6GB増加して、メモリが利用されていることが確認できます。

おわり

Windowsの共有メモリを使って32bitアプリで4GBの壁を超える方法を紹介しました。 とはいえ、AviUtlのような特殊な事情が無いのであれば、64bitアプリとして作り直したほうが良いとは思います。

  1. Windowsの32bitアプリの場合、2GBの壁も別にあります。この壁は64bitWindows上で動作させる場合はLARGEADDRESSAWAREを有効にすることで回避できます。 

  2. OS側でもこの時点ではメモリ領域が確保されるわけではなく、メモリへの書き込みが発生した時点で初めて確保されるようになっています。