Советы на тему о производительности Euphoria


Общие приемы повышения производительности

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

  • Если ваша программа слишком долго раздумывает перед каждым следующим своим шагом, советы ниже, вероятно, не решат всех ее и ваших проблем. Вы должны будете еще раз тщательно продумать алгоритм и поискать пути его упрощения.

  • Простейший путь добавить скорости - отключить проверку типов во время прогона. Включите в свою программу строчку:
               without type_check
    
    в самом начале вашего главного .ex-файла, впереди всех операторов include. В типовом случае вы получите прибавку скорости от 0 до 20 процентов в зависимости от того, какие типы вы определили и какие библиотечные файлы включены в вашу программу. Большинство стандартных библиотечных файлов выполняет определенные программистом проверки типов. Программа, которая полностью свободна от проверки типов, может еще немного выиграть в скорости.

    Кроме того, убедитесь, что удалены или закомментированы любые операторы типа

               with trace
               with profile
               with profile_time
    
    . with trace (даже без любых вызовов trace()), и with profile могут легко снизить скорость на 10% и более. with profile_time может снизить скорость на 1%. К тому же, каждый из этих режимов требует и дополнительной памяти для своего функционирования.

  • Вычисления с целыми числами выполняются быстрее, чем с точными.

  • Объявляйте переменные как integer, а не atom , везде, где возможно, и sequence, а не object, также везде, где возможно. Эти аккуратности добавят вам несколько процентов скорости.

  • В выражениях, где выполняются вычисления с плавающей точкой, следует писать численные константы в форме с десятичной точкой, т.е. когда x имеет величину с плавающей точкой, скажем, x = 9.9

    измените:
               x = x * 5
    
    на:
               x = x * 5.0
    
    Это освободит интерпретатор от необходимости каждый раз преобразовывать целое 5 в точное 5.0 .

  • Euphoria выполняет ускоренную обработку if, elsif и while условий, включающих and и or. Euphoria сразу прекращает дальнейшую проверку условий, если на первом же шаге получен значимый результат. Например, в операторе if:
               if x > 20 and y = 0 then
                   ...
               end if
    
    Проверка "y = 0" должна выполняться, только если условие "x > 20" истинно.

    Для максимизации скорости вы можете упорядочить свои проверки с учетом этой особенности интерпретатора. Сделайте сначала "x > 20", если это менее правдоподобно, чем "y = 0".

    В общем случае, получив условие "A and B", Euphoria не будет вычислять выражение B, когда A ложно (ноль). Аналогично, получив условие, подобное "A or B", Euphoria не будет вычислять B, когда A истинно (не-ноль).

    Простой if-оператор высокооптимизирован. В текущей версии интерпретатора вложенные простые if, которые сравнивают целочисленные величины, обычно несколько быстрее, чем одиночный оператор if с укороченной процедурой, т.е.:

           if x > 20 then
               if y = 0 then
                   ...
               end if
           end if
    

  • Скорость доступа к частным, местным и глобальным переменным в Euphoria одна и та же.

  • На производительности Euphoria никак не сказывается использование заранее определенных поименованных констант вместо тарабарщины из голых цифр. Скорость:
               y = x * MAX
    
    точно та же самая как и для:
               y = x * 1000
    
    когда вы заранее определите:
               constant MAX = 1000
    
  • На производительности никак не сказывается объем комментариев в вашей программе. Комментарии полностью игнорируются. Это игнорирование может стоить нескольких миллисекунд при загрузке вашей программы в оперативную память, но это слишком малая цена, которую приходится платить за дальнейшее простое сопровождение вашей программы, тем более, когда вы связываете вашу программу, все комментарии физически удаляются, так что эта цена становится абсолютным нулем.


Измерения производительности

В любом языке программирования, а особенно в Euphoria, вы должны произвести реальные измерения, прежде чем делать какие-либо выводы о производительности.

Euphoria обеспечивает получение профиля команд, а также и профиля времени (только DOS32). Просмотрите файл refman.doc. Вы зачастую будете удивлены, сняв и изучив профили прогона своей программы. Сосредоточьте свое внимание на тех местах вашей программы, где расходуется значительная часть общего времени прогона (или, как минимум, происходят многократные повторения циклов.) Не стоит переписывать те участки кода, которые занимают 0.01% от общего времени прогона. Обыкновенно в программе обязательно найдется одно место, или всего несколько мест, где код может потребовать пересмотра с точки зрения производительности.

Кроме профилирования вы можете выполнять и прямые измерения с использованием функции time(), например,

        atom t
        t = time()
        for i = 1 to 10000 do
            -- небольшой участок кода здесь
        end for
        ? time() - t

Вы можете таким образом испытывать различные варианты небольших участков кода, чтобы выбрать наилучший по скорости.


Как ускорить выполнение циклов

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


Преобразование умножений в сложения в цикле

Сложение выполняется быстрее умножения. Иногда удается заменить умножение на переменную цикла сложением. Что-нибудь вроде:

        for i = 0 to 199 do
            poke(screen_memory+i*320, 0)
        end for
становится:
        x = screen_memory
        for i = 0 to 199 do
            poke(x, 0)
            x = x + 320 
        end for

Сохранение результатов в переменных
  • Более быстрым является сохранение результата в переменной, чем перевычисление его в более поздний момент. Даже такие простые вещи, как индексирование, или прибавление 1 к переменной могут дать эффект.

  • Когда у вас есть ряд с несколькими уровнями вложения, измените код, подобный:
               for i = 1 to 1000 do
                   y[a][i] = y[a][i]+1
               end for
    
    на:
               ya = y[a]
               for i = 1 to 1000 do
                   ya[i] = ya[i] + 1
               end for
               y[a] = ya
    
    Так вы будете выполнять 2 операции индексирования за один цикл вместо исходных 4х. Операции типа ya = y[a] и y[a] = ya очень короткие. Это просто копирование указателя. Они не требуют копирования всего ряда.

  • Некоторую небольшую цену приходится платить, когда вы создаете новый ряд, используя {a,b,c}. Если возможно, уберите эту операцию из критичного цикла, заменив ее переменной перед циклом, и обращаясь к переменной изнутри цикла.


Прямые вызовы процедуры

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


Операции на рядах

Euphoria позволяет вам обрабатывать большие ряды данных, используя единственный оператор. Это дает вам возможность избегать написания циклов, когда обработка происходит поэлементно, т.е.

        x = {1,3,5,7,9}
        y = {2,4,6,8,10}

        z = x + y
вместо:
        z = repeat(0, 5)  -- если необходимо
        for i = 1 to 5 do
            z[i] = x[i] + y[i]
        end for

В большинстве интерпретаторов намного более быстрой является обработка всего ряда (массива) в одном векторном операторе, чем скалярная обработка в цикле. Так получается из-за больших "накладных расходов" в интерпретаторе на каждый оператор, подлежащий выполнению. Euphoria отличается от других интерпретаторов. Euphoria очень скупа на накладные расходы, поэтому операции на рядах не всегда выигрывают во времени. Единственным решением является практический хронометраж обоих способов. Поэлементные затраты обычно меньше, когда ряд обрабатывается в один оператор, но здесь имеются накладные расходы, связанные с выделением и освобождением памяти под ряды, что снижает реальную скорость за счет динамического распределения памяти, которое дает свои преимущества и некоторые иные возможности.


Некоторые специальные случаи оптимизации

Euphoria автоматически оптимизирует определенные специальные случаи. x и y ниже могут быть переменными или выражениями.

        x + 1      -- быстрее общего x + y
        1 + x      -- быстрее общего y + x
        x * 2      -- быстрее общего x * y
        2 * x      -- быстрее общего y * x
        x / 2      -- быстрее общего x / y
        floor(x/y) -- где x и y целые, быстрее чем x/y
        floor(x/2) -- быстрее, чем floor(x/y)

x ниже простая переменная, y - любая переменная или выражение:

        x = append(x, y)   -- быстрее общего z = append(x, y)
        x = prepend(x, y)  -- быстрее общего z = prepend(x, y)
        x = x & y          -- где x много больше, чем y
                           -- быстрее общего z = x & y
Когда вы пишете цикл, который удлиняет ряд, добавляя или сцепляя с ним новые порции данных, время будет, в общем случае, расти пропорционально квадрату числа (N) элементов, которые вы добавляете. Тем не менее, если вы можете использовать один из специальных случаев оптимизированных форм append(), prepend() или сцепления, перечисленных выше, время будет расти пропорционально всего лишь N (грубо). Эта особенность может сохранить вам громадный кусок времени, если требуется создавать исключительно длинный ряд. (Вы можете также применить repeat(), чтобы сразу достичь максимальной длины ряда, а затем заполнять ряд элементами в цикле, как описано ниже.)


Присвоение с операторами

Для ускорения преобразуйте:

        left-hand-side = left-hand-side op expression
в:
        left-hand-side op= expression
всякий раз, когда left-hand-side содержит как минимум 2 индекса или, по крайней мере, один индекс и один отрезок. Во всех более простых случаях обе формы исполняются за одно и то же время (или очень близкое).


Пиксельно - графические хитрости

  • Режим 19 является наиболее быстрым для анимированной графики и для игр.

  • Видеопамять (в режиме 19) не буферизуется процессором. Для чтения и записи данных на экран обычно уходит больше времени, чем для занесения данных в общую память, которую занимает ваша программа. Это обстоятельство делает эффективными виртуальные экраны, когда вы делаете все обновления изображения в блоке памяти, который получили от функции allocate(), а затем периодически вызываете процедуру mem_copy(), чтобы перенести изображение в реальную экранную память. Таким манером вам никогда не приходится читать из (медленной) экранной памяти.

  • Выводя пиксельные изображения, вы можете обнаружить, что в режимах от 257 и выше вывод быстрый вверху экрана, но медленный внизу.


Текстовые режимы

Вывод текста на экран с использованием puts() или printf() скорее медленный, чем быстрый. Если необходимо, под DOS32, вы можете выполнять его намного быстрее, применив прямую запись в видеопамять, или воспользовавшись процедурой display_text_image(). Накладные расходы на каждый оператор puts() при выводе на экран очень велики, но относительно невелика возрастающая цена на каждый очередной символ. Эти накладные расходы особенно большие с exw(по крайней мере, под Windows 95/98). Linux по скорости вывода текста находится где-то между DOS32 и WIN32. Поэтому может оказаться выгодным приемом сначала подготовить длинную строку, а затем вывести ее всю, вызвав один раз puts(), а не обращаться к puts() для вывода каждого очередного символа. Тем не менее, создание строк длиннее, чем стандартная ширина экрана, не приносит дополнительных выгод.

Низкая скорость вывода текста обусловлена в основном задержками в операционной системе.


Библиотечные процедуры

Ряд общих процедур имеет экстремальную скорость. Вы, вероятно, не сможете выполнить подобную работу быстрее любым другим путем, даже если перейдете на C или ассемблер. Некоторые из этих процедур:

  • mem_copy()
  • mem_set()
  • repeat()

Другие процедуры также очень быстрые, но вы сможете обогнать их в некоторых случаях, когда скорость является критическим параметром. Вариант:

        x = repeat(0,100)
        for i = 1 to 100 do
            x[i] = i
        end for
явлется более быстрым, чем:
        x = {}
        for i = 1 to 100 do
            x = append(x, i)
        end for
так как append() должна занимать и освобождать память по мере роста длины x. С repeat() память для x выделяется один раз в начале. (append() достаточно умна, чтобы не перевыделять память с каждым прибавлением к x. Она выделяет память с некоторым запасом, чтобы уменьшить число распределений памяти.)

Вы можете заменить:

        remainder(x, p)
на:
        and_bits(x, p-1)
для увеличения скорости, когда p является положительной степенью 2. x должно быть неотрицательным целым, которое умещается в 32-бита.

arctan() быстрее, чем arccos() или arcsin().


Поиск

Процедура Euphoria find() является наиболее быстрым способом поиска величины в ряде длиной до 50 элементов. За пределами этого вам могут оказаться необходимыми хэш-таблица (demo\hash.ex) или бинарное дерево (demo\tree.ex).


Сортировка

В большинстве случаев вы можете просто использовать шелл-сортировку, обеспечиваемую процедурой из файла include\sort.e.

Если вам необходимо сортировать громадный объем данных, попытайтесь применить одну из сортировок в файле demo\allsorts.e (к примеру, грейт-сортировку). Если ваши данные не размещаются в памяти, не полагайтесь на автоматический Euphoria своппинг памяти на диск. Напротив, сортируйте по несколько тысяч записей и записывайте их в последовательность временных файлов. Затем объедините отсортированные временные файлы в один большой отсортированный файл.

Если ваши данные состоят только из целых чисел, и они все находятся в пределах довольно узкого диапазона, попробуйте бакет-сортировку из файла demo\allsorts.e.


Использование преимуществ буферизации памяти

По мере роста скорости процессоров разрыв по скорости между кэш-памятью кристалла и основной памятью или DRAM (dynamic random access memory) становится даже больше. У вас может быть на машине 32 Мб DRAM, но на кристалле вы будете иметь что-то около 8K (данные) плюс 8K (инструкции) для Pentium, или 16K (данные) плюс 16K (инструкции) для Pentium с MMX или Pentium II/III. Большинство машин будет также иметь кэш-память "уровня-2" объемом 256K или 512K.

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

Эти эффекты буферизации не так заметны в Euphoria, как в компилирующих языках низкого уровня, но они поддаются измерениям.


Использование машинного кода и C

Euphoria позволяет вам вызывать процедуры, написанные в кодах 32-битной машины Intel. Под WIN32 и Linux вы можете вызывать процедуры C, размещенные в .dll-файлах или .so-файлах, а эти процедуры C могут вызывать ваши процедуры Euphoria. Вам может потребоваться вызов C или машинного кода, если что-то не может быть исполнено непосредственно в Euphoria, или вам нужно разогнать свою программу.

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

Многие программы выполняют некоторые внутренние скрытые операции в своем ядре, занимающие основное время процессора. Если вы можете закодировать все это на C или в машинном коде, выйдя за пределы программы на Euphoria, вы сможете получить скорость, свойственную C, не жертвуя безопасностью и гибкостью Euphoria.

 
Использование транслятора с Euphoria на C

Вы можете получить транслятор с Euphoria на C на Web-узле RDS. Он переведет любую программу Euphoria в набор исходных файлов C, которые вы затем сможете откомпилировать с использованием компилятора C.

Исполняемый файл, который вы получаете с использованием транслятора, выполняет ту же самую работу, но быстрее, чем программа под управлением интерпретатора. Ускорение может составить от нескольких процентов до пятикратного и более.