company_banner

Неинициализированные переменные: ищем ошибки


    Большое количество научных исследований используют код, написанный на языке Фортран. И, к великому сожалению, «научные» приложения тоже не застрахованы от банальных ошибок, таких как неинициализированные переменные. Стоит ли говорить, к чему могут приводить подобные вычисления? Иногда эффект от таких ошибок может довести до «серьёзных прорывов» в науке, или стать причиной действительно больших проблем – кто знает где полученные результаты могут быть использованы (но, мы догадываемся где)? Хотелось бы привести ряд простых и эффективных методов, которые позволят проверить существующий код на Фортране с помощью компилятора Intel и избежать подобных неприятностей.

    Мы будем рассматривать проблемы, связанные с числами с плавающей точкой. Ошибки с неинициализированными переменными достаточно трудно находимы, особенно, если код начинали писать на стандарте Fortran 77. Специфика заключается в том, что даже если мы не объявили переменную, она будет объявляться неявно, в зависимости от первой буквы имени, по, так называемым, правилам неявного определения типов (всё это так же поддерживается в последних стандартах). Буквы от I до N означают тип INTEGER, а остальные буквы — тип REAL. То есть, если в нашем коде неожиданно появляется переменная F, на которую мы что-то умножаем, компилятор не будет выдавать ошибок, а просто сделает F вещественным типом. Вот такой замечательный пример может вполне хорошо скомпилироваться и выполниться:

    program test
      z = f*10
      print *, z, f
    end program test
    

    Как вы понимаете, на экране будет всё, что угодно. У меня так:

    -1.0737418E+09 -1.0737418E+08
    

    Интересно, что в том же стандарте была возможность подобные «игры» с объявлением переменных запрещать, но только в пределах программной единицы, написав implicit none. Правда, если забыть это сделать в каком-то модуле, там так и будут появляться «фантомные» переменные. Любопытно, что я как-то раз видел случайно добавленные символы к имени переменной в расчётах. Видимо, кто-то случайно набирал что-то в блокноте, и часть из них добавилась в код программы при переключении между окнами. В итоге, всё продолжало считаться, и на переменную никто не ругался. Отследить подобные ошибки крайне сложно, особенно если код долгие годы работал без проблем.

    Поэтому, очень рекомендую всегда использовать implicit none и получать ошибки от компилятора о переменных, которые не были явно определены (даже если они и инициализированы и с ними всё хорошо):

    program test
      implicit none
      ...
    end program test
    
    error #6404: This name does not have a type, and must have an explicit type.   [Z]
    error #6404: This name does not have a type, and must have an explicit type.   [F]
    

    Если же мы разбираемся в уже написанном коде, то менять все исходники может быть весьма трудозатратно, поэтому можно воспользоваться опцией компилятора /warn:declarations(Windows) или -warn declarations(Linux). Она выдаст нам предупреждения:

    warning #6717: This name has not been given an explicit type.   [Z]
    warning #6717: This name has not been given an explicit type.   [F]
    

    Когда мы разберёмся со всеми неявными объявленными переменными и убедимся, что ошибок с ними нет, можно переходить к следующей части «Марлезонского балета», а именно поиском неинициализированных переменных.

    Одним из стандартных способов является инициализация компилятором всех переменных некоторым значением, по которому, при работе с переменной, мы сможем легко понять, что разработчик забыл про инициализацию. Значение это должно быть весьма «необычным», а при работе с ним, желательно, останавливать выполнение приложения, чтобы, так сказать, «взять с поличным».

    Весьма логичным является использование «сигнальным» значением SNaN — Signaling NaN (Not-a-Number). Это число с плавающей точкой, имеющее особое представление, и при попытке выполнить любую операцию с ним, мы получим исключение. Стоит сказать, что некая переменная может получить значение NaN и при выполнении определенных операция, например, делении на нуль, умножении нуля на бесконечность, делении бесконечности на бесконечность и так далее. Поэтому, прежде чем переходить к «отлову» неинициализированных переменных, хотелось бы убедиться, что в нашем коде нет никаких исключений, связанных с работой с числами с плавающей точкой.

    Для этого нужно включить опцию /fpe:0 и /traceback (Windows), или –fpe0 и –traceback (Linux), собрать приложение и запустить его. Если всё прошло как обычно, и приложение вышло без генерации исключения, то мы молодцы. Но, вполне возможно, что уже на этом этапе «полезут» разные «непредвиденные моменты». А всё потому, что fpe0 меняет дефолтную работу с исключениями для чисел с плавающей точкой. Если по умолчанию они отключены, и мы спокойно делим на 0, не подозревая об этом, то теперь, будет происходить генерация исключения и остановка выполнения программы. Кстати, не только при делении на 0 (divide-by-zero), но и при переполнении числа с плавающей точкой (floating point overflow), а так же при недопустимых операциях (floating invalid). При этом, численные результаты могут также несколько измениться, так как теперь денормализованные числа будут «сбрасываться» в 0. Это, в свою очередь, может дать весомое ускорение при выполнении вашего приложения, так как с денормализованными числами работа происходит крайне медленно, ну а с нулями – сами понимаете.

    Ещё один интересный момент – возможное получение исключений с опцией fpe0 в результате определённых компиляторных оптимизаций, например, векторизации. Скажем, мы в цикле и делили на значение, если оно не 0, делая проверку if. Возможна ситуация, когда деление всё же будет происходить, потому что компилятор решил, что это будет значительно быстрее, чем использовать маскированные операции. В данном случае мы работаем в спекулятивном режиме.

    Так вот это можно контролировать с помощью опции /Qfp-speculation:strict (Windows) или -fp-speculation=strict (Linux), и отключать подобные оптимизации компилятора при работе с числами с плавающей точкой. Другой способ – изменить всю модель работы через -fp-model strict, что даёт большой отрицательный эффект на общую производительность приложения. Про то, какие модели имеются в компиляторе Intel я уже рассказывал ранее.

    Кстати, можно поробовать и просто уменьшить уровень оптимизации через опции /O1 или /Od на Windows (-O1 и -O0 на Linux).

    Опция traceback просто позволяет получить более детальную информацию о том, где произошла ошибка (имя функции, файл и строчка кода).

    Давайте сделаем тест на Windows, скомпилировав без оптимизации (с опцией /Od):

    program test
      implicit none
      real a,b
      a=0
      b = 1/a
      print *, 'b=', b
    end program test
    

    В итоге на экране мы увидим следующее:

    b=       Infinity
    

    Теперь включаем опцию /fpe:0 и /traceback и получаем ожидаемый exception:

    forrtl: error (73): floating divide by zero
    Image              PC        Routine            Line        Source
    test.exe      00F51050  _MAIN__                    5  test.f90
    …
    

    Такие проблемы нам нужно убрать из нашего кода до начала следующего этапа, а именно, принудительной инициализации значениями SNaN с помощью опции /Qinit:snan,arrays /traceback (Windows) или -init=snan,arrays -traceback (Linux).

    Теперь каждый доступ к неинициализированной переменной приведёт к ошибке времени выполнения:

    forrtl: error (182): floating invalid - possible uninitialized real/complex variable.
    

    На простейшем примере:

    program test
      implicit none
      real a,b
      b = 1/a
      print *, 'b=', b
    end program test
    
    forrtl: error (182): floating invalid - possible uninitialized real/complex variable.
    Image              PC        Routine            Line        Source
    test.exe      00D01061  _MAIN__                     4  test.f90
    …
    

    Немного слов о том, что это за диковинная опция init. Появилась она не так давно, а именно с версии компилятора 16.0 (напомню, что последняя версия компилятора на сегодня – 17.0), и позволяет инициализировать в SNaN следующие конструкции:

    • Статические скаляры и массивы (с атрибутом SAVE)
    • Локальные скаляры и массивы
    • Автоматические (образуемые при вызове функций) массивы
    • Переменные из модулей
    • Динамически выделяемые (с атрибутом ALLOCATABLE) массивы и скаляры
    • Указатели (переменные с атрибутом POINTER)

    Но есть и ряд ограничений, для которых init работать не будет:

    • Переменные в группах EQUIVALENCE
    • Переменные в COMMON блоке
    • Наследуемые типы и их компоненты не поддерживаются, кроме ALLOCATABLE и POINTER
    • Формальные (dummy) аргументы в функциях не инициализируются в SNaN локально. Тем не менее, фактические аргументы, передаваемые в функцию могут быть инициализированы в вызывающей функции.
    • Ссылки в аргументах интринсик-функций и выражениях I/O

    Кстати, опция умеет не только инициализировать значения в SNaN, но и занулять их. Для этого нужно указать /Qinit:zero на Windows (-init=zero на Linux), и будут инициализированы не только типы REAL/COMPLEX, но и целочисленные INTEGER/LOGICAL. Добавляя arrays, мы так же будем инициализировать массивы, а не только скалярные значения.

    Например, опции:

    -init=snan,zero               ! Linux and OS X systems
    /Qinit:snan,zero              ! Windows systems
    

    Инициализируют скаляры типов REAL или COMPLEX значением SNaN, а типы INTEGER или LOGICAL нулями. Следующий пример расширяет действие инициализации ещё и на массивы:

    -init=zero -init=snan –init=arrays       ! Linux and OS X systems
    /Qinit:zero /Qinit:snan /Qinit:arrays    ! Windows systems
    

    В прошлом Intel пытался реализовать подобный функционал через опцию -ftrapuv, но на сегодняшний день она не рекомендуется к использованию и устарела, хотя по задумке, тоже должна была инициализировать значения — не сложилось.

    Кстати, если вы работаете на сопроцессоре Intel Xeon Phi первого поколения (Knights Corner), то опция будет недоступна для вас, так как там нет поддержки SNaN.

    Ну и в конце, примерчик из документации, который мы скомпилируем на Linux со всеми предложенными опциями и найдём неинициализированные переменные в рантайме:

    !  ==============================================================
    !  
    !   SAMPLE SOURCE CODE - SUBJECT TO THE TERMS OF SAMPLE CODE LICENSE AGREEMENT,
    !   http://software.intel.com/en-us/articles/intel-sample-source-code-license-agreement/
    !  
    !   Copyright 2015 Intel Corporation
    !  
    !   THIS FILE IS PROVIDED "AS IS" WITH NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT
    !   NOT LIMITED TO ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
    !   PURPOSE, NON-INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS.
    !  
    !   ===============================================================
    module mymod
      integer, parameter :: n=100
      real                            :: am
      real, allocatable, dimension(:) :: dm
      real, target,      dimension(n) :: em
      real, pointer,     dimension(:) :: fm
    end module mymod
    
    subroutine sub(a, b, c, d, e, m)
      use mymod
      integer, intent(in)               :: m
      real, intent(in),    dimension(n) :: c
      real, intent(in),    dimension(*) :: d
      real, intent(inout), dimension(*) :: e
      real, automatic,     dimension(m) :: f  
      real                              :: a, b
      
      print *, a,b,c(2),c(n/2+1),c(n-1)
      print *, d(1:n:33)   !  first and last elements uninitialized
      print *, e(1:n:30)   !  middle two elements uninitialized
      print *, am, dm(n/2), em(n/2)
      print *, f(1:2)      !  automatic array uninitialized
    
      e(1) = f(1) + f(2)
      em(1)= dm(1) + dm(2)
      em(2)= fm(1) + fm(2)
      b    = 2.*am 
      
      e(2) = d(1) + d(2)
      e(3) = c(1) + c(2)
      a    = 2.*b
    end
    
    program uninit
      use mymod
      implicit none
    
      real, save                       :: a
      real, automatic                  :: b  
      real, save, target, dimension(n) :: c 
      real, allocatable,  dimension(:) :: d
      real,               dimension(n) :: e  
      
      allocate (d (n))
      allocate (dm(n))
      fm => c
      d(5:96) = 1.0
      e(1:20) = 2.0
      e(80:100) = 3.0
      call sub(a,b,c,d,e(:),n/2)
      deallocate(d)
      deallocate(dm)
    end program uninit
    

    Сначала, компилируем с –fpe0 и запускаем:
    $ ifort -O0 -fpe0 -traceback uninitialized.f90; ./a.out
    
      0.0000000E+00 -8.7806177E+13  0.0000000E+00  0.0000000E+00  0.0000000E+00
      0.0000000E+00   1.000000       1.000000      0.0000000E+00
       2.000000      0.0000000E+00  0.0000000E+00   3.000000
      0.0000000E+00  0.0000000E+00  0.0000000E+00
      1.1448686E+24  0.0000000E+00
    

    Видно, что никаких исключений, связанных с операциями надо числами с плавающей точкой в нашем приложении нет, но есть несколько «странных» значений. Будем искать неинициализированные переменные с опцией init:

    $ ifort -O0 -init=snan -traceback uninitialized.f90; ./a.out
                NaN            NaN  0.0000000E+00  0.0000000E+00  0.0000000E+00
      0.0000000E+00   1.000000       1.000000      0.0000000E+00
       2.000000      0.0000000E+00  0.0000000E+00   3.000000
                NaN  0.0000000E+00  0.0000000E+00
      1.1448686E+24  0.0000000E+00
    forrtl: error (182): floating invalid - possible uninitialized real/complex variable.
    Image              PC                Routine            Line        Source
    a.out              0000000000477535  Unknown               Unknown  Unknown
    a.out              00000000004752F7  Unknown               Unknown  Unknown
    a.out              0000000000444BF4  Unknown               Unknown  Unknown
    a.out              0000000000444A06  Unknown               Unknown  Unknown
    a.out              0000000000425DB6  Unknown               Unknown  Unknown
    a.out              00000000004035D7  Unknown               Unknown  Unknown
    libpthread.so.0    00007FC66DD26130  Unknown               Unknown  Unknown
    a.out              0000000000402C11  sub_                       39  uninitialized.f90
    a.out              0000000000403076  MAIN__                     62  uninitialized.f90
    a.out              00000000004025DE  Unknown               Unknown  Unknown
    libc.so.6          00007FC66D773AF5  Unknown               Unknown  Unknown
    a.out              00000000004024E9  Unknown               Unknown  Unknown
    Aborted (core dumped)
    

    Теперь видно, что на строчке 39 мы обращаемся к неинициализированный переменной AM из модуля MYMOD:

    b    = 2.*am
    

    В этом коде есть и другие ошибки, которые я предлагаю найти самим с помощью компилятора Intel. Очень надеюсь, что данный пост будет полезен всем, кто пишет код на Фортране, и ваши приложения пройдут необходимые проверки на неинициализированные переменные ещё до выхода «в свет». На этом спасибо за внимание и до скорых встреч! Всех с наступающим Новым Годом!
    Intel
    Company

    Comments 2

      0
      Сильно ли влияет наличие опций -init=snan,arrays влияет на скорость работы программы?
      Стоит ли использовать их только в так называемом «debug mode», или и для «release mode» тоже подойдет?
        0
        Такого рода опции используются в целях отладки и нахождения ошибок в коде, поэтому их overhead не критичен.
        Но посмотреть возможные исключения через fpe0 можно и с release билдом, чтобы убедиться, что компиляторные оптимизации не приводят к проблемам в вычислениях. Для финального билда их нужно отключать.

      Only users with full accounts can post comments. Log in, please.