Переполнение стека в Фортран 90 - PullRequest
7 голосов
/ 27 апреля 2011

Я написал довольно большую программу на Фортране-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, и операционной системой является Microsoft Server 2008 R2 Enterprise.Согласно systeminfo в приглашении cmd у меня 8 ГБ физической памяти и почти 16 ГБ виртуальной.Насколько я понимаю, сумма всех моих массивов и матриц не должна превышать 100 МБ - около 5,5 М integer(4) и 2,5 М real(8) (что, по моему мнению, должно составлять только около 44 МБ, но давайте будем честными)и добавьте еще 50 МБ для служебных данных).

Я использую компилятор Intel Fortran, интегрированный с Microsoft Visual Studio 2008.


Добавление некоторого фактического исходного кода для пояснения

! 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)

- это фактический вызов подпрограммы.Большие массивы posc, bmtrx и aa - все остальные по крайней мере на порядок меньше (если не больше).posc равно INTEGER(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)
...

И происходит сбой до последней строки выше.

Ответы [ 6 ]

7 голосов
/ 28 апреля 2011

По просьбе Стиберта я просто подведу итоги разговора в комментариях здесь, где он немного более заметен, хотя ответ MSB уже подходит к сути проблемы.

В техническом программировании, где процедуры часто имеют большие локальные массивы для промежуточных вычислений, это часто случается.Локальные переменные обычно хранятся в стеке, который обычно (и вполне разумно) составляет небольшую долю общей системной памяти - обычно порядка 10 МБ или около того.Когда размеры локальной переменной превышают размер стека, вы видите в точности описанные здесь симптомы - переполнение стека, возникающее после вызова соответствующей подпрограммы, но до ее первого исполняемого оператора.

Так что, когда возникает эта проблема,Лучше всего найти соответствующие большие локальные переменные и решить, что делать.В этом случае, по крайней мере, переменные belm и dstrain становились достаточно значительными.

Как только переменные найдены, и вы подтвердили, что это проблема, есть несколько вариантов.Как указывает MSB, если вы можете сделать ваши массивы меньше, это один из вариантов.Кроме того, вы можете увеличить размер стека;под linux это делается с ulimit -s [newsize].Это на самом деле просто откладывает проблему, и на машинах с Windows нужно сделать что-то другое.

Другой класс способов избежать этой проблемы - не помещать большие данные в стек, а в остальное.памяти («куча»).Вы можете сделать это, присвоив массивам атрибут save (в C, static);это помещает переменную в кучу и, таким образом, делает значения постоянными между вызовами.Недостатком является то, что это потенциально меняет поведение подпрограммы, и означает, что подпрограмма не может быть использована рекурсивно, и, аналогично, не является потокобезопасной (если вы когда-либо находитесь в положении, когда несколько потоков будут входить в подпрограмму одновременно, оникаждый увидит одну и ту же копию локального варианта и, возможно, перезапишет результаты друг друга).Плюс в том, что он легкий и очень портативный - он должен работать везде.Однако это будет работать только с локальными переменными фиксированного размера;если временные массивы имеют размеры, которые зависят от входных данных, вы не сможете этого сделать (поскольку больше не будет единственной переменной для сохранения; она может иметь разный размер при каждом вызове процедуры).

Существуют специфичные для компилятора опции, которые помещают все массивы (или все массивы, превышающие некоторый заданный размер) в кучу, а не в стек;каждый известный мне компилятор Fortran имеет опцию для этого.Для ifort, используемого в посте OP, это -heap-arrays в Linux или /heap-arrays для Windows.Для gfortran это может быть по умолчанию.Это хорошо для того, чтобы убедиться, что вы знаете, что происходит, но это значит, что для каждого компилятора должны быть разные заклинания, чтобы убедиться, что ваш код работает.

Наконец, вы можете сделать выделяемые массивы распределенными.Выделенная память уходит в кучу;но переменная, которая указывает на них, находится в стеке, поэтому вы получаете преимущества обоих подходов.Кроме того, это полностью стандартный фортран и поэтому полностью переносимый.Недостатком является то, что это требует изменения кода.Кроме того, процесс распределения может занять нетривиальные промежутки времени;так что если вы собираетесь вызывать рутину миллионы раз, вы можете заметить, что это немного замедляет процесс.(Эту возможную регрессию производительности легко исправить, хотя; если вы будете вызывать ее миллионы раз с массивами одинакового размера, у вас может быть необязательный аргумент для передачи предварительно выделенного локального массива и его использования вместо этого, так чтоВы выделяете / освобождаете только один раз).

Выделение / освобождение каждый раз будет выглядеть следующим образом:

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)
2 голосов
/ 27 апреля 2011

Не вся память создается при запуске программы.Когда вы вызываете подпрограмму, исполняемый файл создает память, которая нужна подпрограмме для локальных переменных.Обычно массивы с простыми объявлениями, которые являются локальными для этой подпрограммы - ни выделяемыми, ни указателями - размещаются в стеке.Вы могли бы просто запустить пространство стека, когда вы достигли этих объявлений.Возможно, вы достигли предела 2 ГБ в 32-битной ОС с некоторым массивом.Иногда исполняемые операторы неявно создают временный массив в стеке.

Возможные решения: 1) уменьшить массивы (не привлекательно), 2) увеличить стек), 3) некоторые компиляторы имеют возможность переключаться с размещения массивов в стеке на их динамическое распределение, аналогичнометод, используемый для «выделения», 4) идентифицирует большие массивы и делает их размещаемыми.

1 голос
/ 27 апреля 2011

Используете ли вы распараллеливание?Это может быть проблемой со статически объявленными массивами.Попробуйте все большие массивы сделать ALLOCATABLE, в противном случае они будут помещены в стек в виде параллельных потоков или потоков OpenMP.

1 голос
/ 27 апреля 2011

Единственная проблема, с которой я столкнулся с подобным тестовым кодом, - это ограничение выделения 2 ГБ для 32-разрядной компиляции. Когда я превышаю его, я получаю сообщение об ошибке в строке 419 в winsig.c

2GB Allocation Limit Error

Вот код теста

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() для фиктивных переменных.

1 голос
/ 27 апреля 2011

Стек - это область памяти, где хранится информация, необходимая для возврата из функции, и информация, локально определенная в функции. Таким образом, переполнение стека может указывать на то, что у вас есть функция, которая вызывает другую функцию, которая, в свою очередь, вызывает другую функцию и т. Д.

Я не знаком с Фортраном (больше), но другая причина может заключаться в том, что эти функции объявляют тонны локальных переменных или, по крайней мере, переменных, которые требуют много места.

Последнее: стек обычно довольно мал, поэтому априори не важно, сколько памяти у машины. Должно быть довольно просто дать указание компоновщику увеличить размер стека, по крайней мере, если вы уверены, что это просто недостаток места, а не ошибка в вашем приложении.

Редактировать: вы используете рекурсию в своей программе? Рекурсивные вызовы могут очень быстро проникать в стек.

Редактировать: посмотрите на это : (выделено мной)

В Windows пространство стека зарезервировано для программы устанавливается с помощью опция компилятора / Fn, где n количество байтов. Дополнительно, резервный размер стека может быть указывается через Visual Studio IDE, которая добавляет Microsoft Linker опция / STACK: к команде компоновщика линия. Чтобы установить это, перейдите в свойство Страницы> Конфигурация Свойства> Линкер> Система> Резерв стека Размер. Там вы можете указать стек размер в байтах в десятичном или Нотация на языке C Если не указано, размер стека по умолчанию составляет 1 МБ .

0 голосов
/ 04 апреля 2017

Для меня проблема заключалась в резервном размере стека.Я изменил зарезервированный размер стека с 0 на 100000000 и перекомпилировал код.Код теперь работает без сбоев.

enter image description here

...