Повышение производительности


Общие советы

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

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

  • Простейший путь немного разогнать программу - это выключение проверки типов во время прогона. Вставьте в код строку:
               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, когда он "увидит", что x не целое, а атом.

  • 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" B не будет оцениваться, когда A истинно (не-ноль).

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

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

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

  • Если вы определили константу, то её использование не проигрывает по скорости против использования литеральных чисел, то есть, скорость:
               y = x * MAX
    
    точно такая же, как и здесь:
               y = x * 1000
    
    если вы ранее выполнили:
               constant MAX = 1000
    
  • Никакие комментарии не влияют на скорость исполнения вашей программы. Комментарии полностью игнорируются. Они не исполняются никаким образом. Их игнорирование займёт несколько миллисекунд при запуске вашей программы, но это очень хорошая цена за будущее лёгкое сопровождение ясно и подробно откомментированной программы. Тем более, что при связывании вашей программы или её трансляции на C, все комментарии удаляются, и цена становится просто нулевой.


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

В любой системе программирования, а собенно в 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(), чтобы зарезервировать память под максимальный необходимый размер ряда, а затем заполнять ряд элементами в цикле, как это обсуждается ниже.)


Присваивание с операцией

Чтобы повысить скорость, преобразовывайте:

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


Советы по пиксельной графике

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

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

  • Работая с пикселами, вы можете заметить, что режимы 257 и выше более быстрые около верхней границы экрана, но медленнее у низа экрана. (Прим.пер. Возможно, это положение устарело, так как в Open Watcom внедрена гораздо более быстрая графическая библиотека DOS32, чем была в коммерческой версии, которой традиционно пользуется автор.)


Советы по текстовому режиму

Вывод текста на экран с помощью puts() или printf() не блещет скоростью. Если необходимо, под DOS32, вы можете выводить текст гораздо быстрее путём прямой записи в видеопамять, или с использованием display_text_image(). Имеются очень большие накладные расходы с каждым puts() на экран, и относительно малая возрастающая цена на символ. И эти расходы с exw (WIN32) особенно высоки (по крайней мере, под Windows 95/98/ME). Linux и FreeBSD находятся где-то между 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 (например, сортировку great). Если ваши данные не умещаются в памяти, не полагайтесь на способность Euphoria к автоматическому свопингу данных на диск. Вместо этого сортируйте по несколько тысяч записей за один раз, выводя результаты последовательно во временные файлы. А затем соедините все отсортированные временные файлы в один большой результирующий отсортированный файл.

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


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

По мере роста скорости процессоров разрыв между скоростью буферной памяти на кристалле и скоростью главного ОЗУ становится всё больше. У вас может быть 256 Mb ОЗУ на компьютере, но буфер на кристалле будет около 8K (данные) плюс 8K (инструкции) с процессором Pentium, или 16K (данные) плюс 16K (инструкции) с Pentium MMX или Pentium II/III. Большинство машин будет иметь также буфер "уровня-2" на 256K или 512K.

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

Эти буферные эффекты не так заметны с Euphoria, как с низкоуровневыми компилируемыми языками, но они всё-таки доступны для измерений.


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

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

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

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

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

Начиная с версии 3.0, полный транслятор с Euphoria на C включён в общедоступный пакет системы. Транслятор преобразует любую программу Euphoria в набор файлов исходного кода C, которые могут быть откомпилированы с использованием компилятора C.

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