10

私は Fortran 90 でかなり大きなプログラムを書きました。それはかなり長い間美しく動作していましたが、今日はそれを一段階上げて問題のサイズを大きくしようとしました (それが研究用の非標準 FE ソルバーである場合、誰にも役立ちます...) 今、「スタックオーバーフロー」エラーメッセージが表示され、プログラムは当然、作業に役立つものを何も提供せずに終了します。

プログラムは、関連するすべての配列と行列の設定から開始し、その後、これに関する数行の統計をログ ファイルに出力します。私の新しいより大きな問題でも、これはうまく機能しますが(少し遅いですが)、「数の計算」が進むにつれて失敗します。

私を混乱させるのは、その時点ですべてがすでに割り当てられていることです(そして、エラーなしで機能しました)。スタックが何であるかは完全にはわかりません (ウィキペディアとここのいくつかのトレッドは、コンピューターの「舞台裏」の仕組みについて非常に基本的な知識しか持っていないため、あまり役に立ちませんでした)。

たとえば、次のように初期化されたいくつかの配列があるとします。

INTEGER,DIMENSION(64) :: IA
REAL(8),DIMENSION(:,:),ALLOCATABLE :: AA, BB

いくつかの初期化ルーチン(つまり、ファイルからの入力の読み取りなど)が次のように割り当てられた後、次のように割り当てられます(固定サイズのIAのサブルーチンに簡単に渡すために、いくつかのサイズ整数を保存します):

ALLOCATE( AA(N1,N2) , BB(N1,N2) )
IA(1) = N1
IA(2) = N2

これは基本的に最初の部分で発生することであり、これまでのところ問題ありません。しかし、次にサブルーチンを呼び出すと

CALL ROUTINE_ONE(AA,BB,IA)

そして、ルーチンは次のようになります (派手なものはありません):

SUBROUTINE ROUTINE_ONE(AA,BB,IA)
IMPLICIT NONE
INTEGER,DIMENSION(64) :: IA
REAL(8),DIMENSION(IA(1),IA(2)) :: AA, BB
...
do lots of other stuff
...
END SUBROUTINE ROUTINE_ONE

今、私はエラーが発生します!画面への出力には次のように表示されます。

forrtl: severe (170): Program Exception - stack overflow

ただし、デバッガーでプログラムを実行すると、419 行目で、というファイルwinsig.c(私のファイルではなく、おそらくコンパイラーの一部?) で中断します。呼び出されたルーチンの一部のようであり、sigreterror:呼び出されたのはデフォルトのケースであり、テキストを返しますInvalid signal or error。これに奇妙なことにコメント行が添付されています/* should never happen, but compiler can't tell */...?

だから私の質問は、なぜこれが起こるのか、そして実際に何が起こっているのかということだと思います. 関連するすべてのメモリを割り当てることができる限り、私は大丈夫だと思いましたか? サブルーチンの呼び出しは、引数のコピーを作成しますか、それとも単にそれらへのポインタを作成しますか? 答えがコピーである場合、問題がどこにあるのかがわかります。もしそうなら、それを回避する方法についてのアイデアはありますか?

私が解決しようとしている問題は大きいですが、決して正気ではありません。標準的な FE ソルバーは、現在の問題よりも大きな問題を処理できます。Dell PowerEdge 1850 でプログラムを実行しており、OS は Microsoft Server 2008 R2 Enterprise です。プロンプトによるとsysteminfocmd8 GB の物理メモリとほぼ 16 GB の仮想メモリがあります。私が理解している限り、すべての配列とマトリックスの合計は、おそらく 100MB を超えてはならない - 約 5.5Minteger(4)と 2.5M real(8)(私によれば、これは約 44MB しかないはずですが、公平を期して、オーバーヘッドのためにさらに 50MB を追加しましょう) )。

Microsoft Visual Studio 2008 に統合された Intel Fortran コンパイラを使用しています。


少し明確にするために実際のソースコードを追加する

! Update continuum state
CALL UpdateContinuumState(iTask,iArray,posc,dof,dof_k,nodedof,elm,&
                    bmtrx,detjac,w,mtrlprops,demtrx,dt,stress,strain,effstrain,&
                    effstress,aa,fi,errmsg)

ルーチンへの実際の呼び出しです。大きな配列はposcbmtrxおよびaa- 他のすべてのものは、少なくとも 1 桁小さい (それ以上ではないにしても) です。poscINTEGER(4)ありbmtrx、でaaあるREAL(8)

SUBROUTINE UpdateContinuumState(iTask,iArray,posc,dof,dof_k,nodedof,elm,bmtrx,&
                    detjac,w,mtrlprops,demtrx,dt,stress,strain,effstrain,&
                    effstress,aa,fi,errmsg)

    IMPLICIT NONE

    !I/O
    INTEGER(4) :: iTask, errmsg
    INTEGER(4) :: iArray(64)
    INTEGER(4),DIMENSION(iArray(15),iArray(15),iArray(5)) :: posc
    INTEGER(4),DIMENSION(iArray(22),iArray(21)+1) :: nodedof
    INTEGER(4),DIMENSION(iArray(29),iArray(3)+2) :: elm
    REAL(8),DIMENSION(iArray(14)) :: dof, dof_k
    REAL(8),DIMENSION(iArray(12)*iArray(17),iArray(15)*iArray(5)) :: bmtrx
    REAL(8),DIMENSION(iArray(5)*iArray(17)) :: detjac
    REAL(8),DIMENSION(iArray(17)) :: w
    REAL(8),DIMENSION(iArray(23),iArray(19)) :: mtrlprops
    REAL(8),DIMENSION(iArray(8),iArray(8),iArray(23)) :: demtrx
    REAL(8) :: dt
    REAL(8),DIMENSION(2,iArray(12)*iArray(17)*iArray(5)) :: stress
    REAL(8),DIMENSION(iArray(12)*iArray(17)*iArray(5)) :: strain
    REAL(8),DIMENSION(2,iArray(17)*iArray(5)) :: effstrain, effstress
    REAL(8),DIMENSION(iArray(25)) :: aa
    REAL(8),DIMENSION(iArray(14)) :: fi 

    !Locals
    INTEGER(4) :: i, e, mtrl, i1, i2, j1, j2, k1, k2, dim, planetype, elmnodes, &
        Nec, elmpnodes, Ndisp, Nstr, Ncomp, Ngpt, Ndofelm
    INTEGER(4),DIMENSION(iArray(15)) :: doflist
    REAL(8),DIMENSION(iArray(12)*iArray(17),iArray(15)) :: belm
    REAL(8),DIMENSION(iArray(17)) :: jelm
    REAL(8),DIMENSION(iArray(12)*iArray(17)*iArray(5)) :: dstrain
    REAL(8),DIMENSION(iArray(12)*iArray(17)) :: s
    REAL(8),DIMENSION(iArray(17)) :: ep, es, dep
    REAL(8),DIMENSION(iArray(15),iArray(15)) :: kelm
    REAL(8),DIMENSION(iArray(15)) :: felm

    dim       = iArray(1)
...

そして、上記の最後の行の前で失敗します。

4

6 に答える 6

9

steabert の要求に従って、MSB の回答が既に問題の要点に達しているにもかかわらず、ここのコメントで会話を要約します。

プロシージャが中間計算用に大きなローカル配列を持つことが多いテクニカル プログラミングでは、これが頻繁に発生します。ローカル変数は通常、スタックに格納されます。これは通常、システム メモリ全体のごく一部 (通常は 10MB 程度) です。ローカル変数のサイズがスタック サイズを超えると、まさにここで説明した症状が見られます。つまり、関連するサブルーチンの呼び出し後、最初の実行ステートメントの前にスタック オーバーフローが発生します。

したがって、この問題が発生した場合の最善の方法は、関連する大きなローカル変数を見つけて、何をすべきかを決定することです。この場合、少なくとも変数 belm と dstrain はかなり大きくなっています。

変数が特定され、それが問題であることを確認したら、いくつかのオプションがあります。MSB が指摘しているように、配列を小さくできるのであれば、それは 1 つのオプションです。または、スタック サイズを大きくすることもできます。Linux では、ulimit -s [newsize]. ただし、それは実際には問題を先延ばしするだけであり、Windows マシンでは別のことを行う必要があります。

この問題を回避するもう 1 つの方法は、大きなデータをスタックに置くのではなく、残りのメモリ (「ヒープ」) に置くことです。配列に属性を与えることでそれを行うことができsaveます(Cでは、static); これにより、変数がヒープに配置され、呼び出し間で値が永続化されます。欠点は、これによりサブルーチンの動作が変わる可能性があり、サブルーチンを再帰的に使用できないことを意味し、同様にスレッドセーフではありません (複数のスレッドが同時にルーチンに入る位置にいる場合、それらはそれぞれがローカル変数の同じコピーを参照し、互いの結果を上書きする可能性があります)。利点は、簡単で移植性が高いことです。どこでも機能するはずです。ただし、これは固定サイズのローカル変数でのみ機能します。一時配列のサイズが入力に依存する場合、これを行うことはできません (保存する単一の変数がなくなるため、プロシージャが呼び出されるたびに異なるサイズになる可能性があるため)。

すべての配列 (または特定のサイズより大きいすべての配列) をスタックではなくヒープに配置するコンパイラ固有のオプションがあります。私が知っているすべての Fortran コンパイラには、このためのオプションがあります。OP投稿で使用されているifortの場合-heap-arrays、Linuxまたは/heap-arraysWindows用です。gfortran の場合、これが実際のデフォルトである可能性があります。これは、何が起こっているのかを確実に把握するのに役立ちますが、コードが確実に機能するようにするには、コンパイラごとに異なる呪文を使用する必要があることを意味します。

最後に、問題のある配列を割り当て可能にすることができます。割り当てられたメモリはヒープになります。しかし、それらを指す変数はスタック上にあるため、両方のアプローチの利点が得られます。また、これは完全に標準の fortran であるため、完全に移植可能です。欠点は、コードの変更が必要なことです。また、割り当てプロセスにはかなりの時間がかかる場合があります。そのため、ルーチンを何十億回も呼び出す場合は、これにより処理がわずかに遅くなることに気付くかもしれません。(ただし、このパフォーマンス低下の可能性は簡単に修正できます。同じサイズの配列で何億回も呼び出す場合は、オプションの引数を使用して、事前に割り当てられたローカル配列を渡し、代わりにそれを使用することができます。一度だけ割り当て/割り当て解除します)。

毎回の割り当て/割り当て解除は次のようになります。

SUBROUTINE UpdateContinuumState(iTask,iArray,posc,dof,dof_k,nodedof,elm,bmtrx,&
                    detjac,w,mtrlprops,demtrx,dt,stress,strain,effstrain,&
                    effstress,aa,fi,errmsg)

    IMPLICIT NONE

    !...arguments.... 


    !Locals
    !...
    REAL(8),DIMENSION(:,:), allocatable :: belm
    REAL(8),DIMENSION(:), allocatable :: dstrain

    allocate(belm(iArray(12)*iArray(17),iArray(15))  
    allocate(dstrain(iArray(12)*iArray(17)*iArray(5))

    !... work

    deallocate(belm)
    deallocate(dstrain)

サブルーチンが多くの作業を行う場合 (たとえば、実行に数秒かかる場合)、いくつかの割り当て/割り当て解除によるオーバーヘッドは無視できることに注意してください。そうでない場合で、オーバーヘッドを回避したい場合は、事前に割り当てられたワークスペースにオプションの引数を使用すると、次のようになります。

SUBROUTINE UpdateContinuumState(iTask,iArray,posc,dof,dof_k,nodedof,elm,bmtrx,&
                    detjac,w,mtrlprops,demtrx,dt,stress,strain,effstrain,&
                    effstress,aa,fi,errmsg,workbelm,workdstrain)

    IMPLICIT NONE

    !...arguments.... 
    real(8),dimension(:,:), optional, target :: workbelm
    real(8),dimension(:), optional, target :: workdstrain
    !Locals
    !...

    REAL(8),DIMENSION(:,:), pointer :: belm
    REAL(8),DIMENSION(:), pointer :: dstrain

    if (present(workbelm)) then
       belm => workbelm
    else
       allocate(belm(iArray(12)*iArray(17),iArray(15))
    endif
    if (present(workdstrain)) then
       dstrain => workdstrain
    else
       allocate(dstrain(iArray(12)*iArray(17)*iArray(5))
    endif

    !... work

    if (.not.(present(workbelm))) deallocate(belm)
    if (.not.(present(workdstrain))) deallocate(dstrain)
于 2011-04-28T12:48:35.513 に答える
4

プログラムの起動時にすべてのメモリが作成されるわけではありません。サブルーチンを呼び出すと、実行可能ファイルは、サブルーチンがローカル変数に必要とするメモリを作成しています。通常、そのサブルーチンにローカルな単純な宣言 (割り当て可能でもポインターでもない) を持つ配列は、スタックに割り当てられます。これらの宣言に到達したときに、単にスタック領域が不足している可能性があります。一部のアレイを使用する 32 ビット OS で 2GB の制限に達した可能性があります。実行可能ステートメントは、スタック上に一時配列を暗黙的に作成することがあります。

可能な解決策:1)配列を小さくする(魅力的ではない)、2)スタックを大きくする)、3)一部のコンパイラには、「割り当て」に使用される方法と同様に、配列をスタックに配置することから動的に割り当てることに切り替えるオプションがあります、4) 大きな配列を識別し、割り当て可能にします。

于 2011-04-27T15:29:47.770 に答える
2

並列化を使用していますか?これは、静的に宣言された配列で問題になる可能性があります。すべての大きな配列を ALLOCATABLE にしてみてください。そうしないと、自動並列または OpenMP スレッドでスタックに配置されます。

于 2011-04-27T16:44:44.320 に答える
2

同様のテスト コードで遭遇した唯一の問題は、32 ビット コンパイルの 2Gb 割り当て制限です。それを超えると、winsig.c の 419 行目にエラー メッセージが表示されます。

2GB 割り当て制限エラー

ここにテストコードがあります

program FortranCon

implicit none

! Variables
INTEGER :: IA(64), S1
REAL(8), DIMENSION(:,:), ALLOCATABLE :: AA, BB
REAL(4) :: S2
INTEGER, PARAMETER :: N = 10960
IA(1)=N
IA(2)=N

ALLOCATE( AA(N,N), BB(N,N) )
AA(1:N,1:N) = 1D0
BB(1:N,1:N) = 2D0

CALL TEST(AA,BB,IA)

S1 = SIZEOF(AA)                 !Size of each array
S2 = 2*DBLE(S1)/1024/1024       !Total size for 2 arrays in Mb

WRITE (*,100) S2, ' Mb'         ! When allocation reached 2Gb then
100 FORMAT (F8.1,A)                 ! exception occurs in Win32

DEALLOCATE( AA, BB )

end program FortranCon


SUBROUTINE TEST(AA,BB,IA)
IMPLICIT NONE
INTEGER, DIMENSION(64),INTENT(IN) :: IA    
REAL(8), DIMENSION(IA(1),IA(2)),INTENT(INOUT) :: AA,BB

... !Do stuff with AA,BB        
END SUBROUTINE

正常N=10960に実行されると、 が表示され1832.9 Mbます。N=11960それとクラッシュします。もちろん、x64でコンパイルすると問題なく動作します。各配列には 8*N^2 バイトのストレージがあります。役立つかどうかはわかりませんがINTENT()、ダミー変数のキーワードを使用することをお勧めします。

于 2011-04-27T01:17:15.120 に答える
2

スタックは、関数から返されるために必要な情報が格納されるメモリ領域であり、関数でローカルに定義された情報が格納されます。そのため、スタック オーバーフローは、別の関数を呼び出す関数があり、その関数が別の関数を呼び出すなどのことを示している可能性があります。

私は (もう) Fortran に精通していませんが、別の原因として、これらの関数が大量のローカル変数、または少なくとも多くの場所を必要とする変数を宣言している可能性があります。

最後の 1 つ: 通常、スタックはかなり小さいため、マシンに搭載されているメモリの量はアプリオリには関係ありません。スタック サイズを増やすようリンカに指示するのは非常に簡単なはずです。少なくとも、それがスペース不足であり、アプリケーションのバグではないことが確実な場合は、そうする必要があります。

編集: プログラムで再帰を使用しますか? 再帰呼び出しは、スタックを非常に速く食い尽くす可能性があります。

編集:これを見てください:(私の強調)

Windows では、プログラム用に予約されるスタック領域は、/Fn コンパイラ オプションを使用して設定されます。n はバイト数です。さらに、スタックの予約サイズは、Microsoft リンカー オプション /STACK: をリンカー コマンド ラインに追加する Visual Studio IDE を介して指定できます。これを設定するには、[Property Pages] > [Configuration Properties] > [Linker] > [System] > [Stack Reserve Size] に移動します。そこでは、10 進数または C 言語表記のいずれかでスタック サイズをバイト単位で指定できます。指定しない場合 、デフォルトのスタック サイズは 1MBです。

于 2011-04-26T20:21:50.543 に答える