Сколько циклов может быть в программе

Сколько циклов может быть в программе thumbnail

При решении задач может возникнуть необходимость повторить одни и те же действия несколько или множество раз. В программировании блоки кода, которые требуется повторять не единожды, оборачиваются в специальные конструкции – циклы. У циклов выделяют заголовок и тело. Заголовок определяет, до каких пор или сколько раз тело цикла будет выполняться. Тело содержит выражения, которые выполняются, если в заголовке цикла выражение вернуло логическую истину (True, не ноль). После того как достигнута последняя инструкция тела, поток выполнения снова возвращается к заголовку цикла. Снова проверяется условие выполнения цикла. В зависимости от результата тело цикла либо повторяется, либо поток выполнения переходит к следующему выражению после всего цикла.

В языке программирования Паскаль существует три вида циклических конструкций.

Блок схемы циклов

Цикл for

Часто цикл for называют циклом со счетчиком. Этот цикл используется, когда число повторений не связано с тем, что происходит в теле цикла. Т.е. количество повторений может быть вычислено заранее (хотя оно не вычисляется).

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

Цикл for существует в двух формах:

for счетчик:=значение to конечное_значение do
тело_цикла;
for счетчик:=значение downto конечное_значение do
тело_цикла;

Счетчик – это переменная любого из перечисляемых типов (целого, булевого, символьного, диапазонного, перечисления). Начальные и конечные значения могут быть представлены не только значениями, но и выражениями, возвращающими совместимые с типом счетчика типы данных. Если между начальным и конечным выражением указано служебное слово to, то на каждом шаге цикла значение параметра будет увеличиваться на единицу. Если же указано downto, то значение параметра будет уменьшаться на единицу.

Количество итераций цикла for известно именно до его выполнения, но не до выполнения всей программы. Так в примере ниже, количество выполнений цикла определяется пользователем. Значение присваивается переменной, а затем используется в заголовке цикла. Но когда оно используется, циклу уже точно известно, сколько раз надо выполниться.

var
i, n: integer;
 
begin
write (‘Количество знаков: ‘);
readln (n);
 
for i := 1 to n do
write (‘(*) ‘);
 
readln
end.

Цикл while

Цикл while является циклом с предусловием. В заголовке цикла находится логическое выражение. Если оно возвращает true, то тело цикла выполняется, если false – то нет.

Когда тело цикла было выполнено, то ход программы снова возвращается в заголовок цикла. Условие выполнения тела снова проверяется (находится значение логического выражения). Тело цикла выполнится столько раз, сколько раз логическое выражение вернет true. Поэтому очень важно в теле цикла предусмотреть изменение переменной, фигурирующей в заголовке цикла, таким образом, чтобы когда-нибудь обязательно наступала ситуация false. Иначе произойдет так называемое зацикливание, одна из самых неприятных ошибок в программировании.

var
i, n: integer;
 
begin
write (‘Количество знаков: ‘);
readln (n);
 
i := 1;
while i <= n do begin
write (‘(*) ‘);
i := i + 1
end;
 
readln
end.

Цикл repeat

Цикл while может не выполниться ни разу, если логическое выражение в заголовке сразу вернуло false. Однако такая ситуация не всегда может быть приемлемой. Бывает, что тело цикла должно выполниться хотя бы один раз, не зависимо оттого, что вернет логическое выражение. В таком случае используется цикл repeat – цикл с постусловием.

В цикле repeat логическое выражение стоит после тела цикла. Причем, в отличие от цикла while, здесь всё наоборот: в случае true происходит выход из цикла, в случае false – его повторение.

var
i, n: integer;
 
begin
write (‘Количество знаков: ‘);
readln (n);
 
i := 1;
repeat
write (‘(*) ‘);
i := i + 1
until i > n;
 
readln
end.

В примере, даже если n будет равно 0, одна звездочка все равно будет напечатана.

Источник

Я иду на риск, публикуя это, но я думаю, что ответ:

между 550 и 575

с настройками по умолчанию в Visual Studio 2015

я создал небольшую программу, которая генерирует вложенные for петли…

for (int i0=0; i0<10; i0++)
{
for (int i1=0; i1<10; i1++)
{


for (int i573=0; i573<10; i573++)
{
for (int i574=0; i574<10; i574++)
{
Console.WriteLine(i574);
}
}


}
}

для 500 вложенных циклов программа все еще может быть скомпилирована. С 575 петлями компилятор выручает:

предупреждение анализатор AD0001 – Microsoft.CodeAnalysis.CSharp.Диагностика.SimplifyTypeNames.CSharpSimplifyTypeNamesDiagnosticanalyzer ‘бросил исключение типа’ системы.InsufficientExecutionStackException ‘with message’ недостаточно стека для безопасного продолжения выполнения программы. Это может произойти из-за слишком большого количества функций в стеке вызовов или функции в стеке, использующей слишком много пространства стека.’.

с базовым сообщением компилятора

ошибка CS8078: выражение слишком долго или сложно компилировать

конечно, это чисто гипотетически результат. Если самый внутренний цикл делает больше, чем Console.WriteLine, то меньше вложенных циклов может быть возможно до превышения размера стека. Кроме того, это не может быть строго техническим ограничением, в том смысле, что могут быть скрытые настройки для увеличения максимального размера стека для “анализатора”, который упоминается в сообщении об ошибке, или (при необходимости) для результирующего исполняемого файла. Этот часть ответа, однако, оставлена людям, которые знают C# в глубине.

в ответ на вопрос в комментариях:

мне было бы интересно увидеть этот ответ расширен, чтобы “доказать” экспериментально, можно ли поместить 575 локальных переменных в стек, если они не используется в for-loops, и/или можно ли поставить 575 невложенной for-петли в одном функция

в обоих случаях, ответ: Да, это возможно. При заполнении метода 575 автоматически генерируемых операторов

int i0=0;
Console.WriteLine(i0);
int i1=0;
Console.WriteLine(i1);

int i574=0;
Console.WriteLine(i574);

он все еще может быть скомпилирован. Все остальное удивило бы меня. Размер стека, который требуется для int переменные – это всего 2,3 КБ. Но мне было любопытно, и чтобы проверить дальнейшие пределы, я увеличил это число. В конце концов, это произошло не компиляции вызывает ошибку

ошибка CS0204: разрешено только 65534 локальных объекта, включая созданные компилятором

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

точно так же, 575 невложеннойfor-петли, как в

for (int i0=0; i0<10; i0++)
{
Console.WriteLine(i0);
}
for (int i1=0; i1<10; i1++)
{
Console.WriteLine(i1);
}

for (int i574=0; i574<10; i574++)
{
Console.WriteLine(i574);
}

также может быть скомпилирован. Здесь я также попытался найти предел и создал больше этих циклов. В частности, я не был уверен будут ли переменные цикла в этом случае также считать als “локальными”, потому что они находятся в своих собственных { block }. Но все же, более 65534 не представляется возможным. Наконец, я добавил тест, состоящий из 40000 петель узора

for (int i39999 = 0; i39999 < 10; i39999++)
{
int j = 0;
Console.WriteLine(j + i39999);
}

который содержал дополнительную переменную на цикл, но они, похоже, также считаются “локальными”, и это не удалось скомпилировать.

Итак, чтобы подвести итог: предел ~550 действительно вызван глубина вложенности из петель. Это также было указано в сообщении об ошибке

ошибка CS8078: выражение слишком длинное или сложное для компиляции

The документация ошибки CS1647 к сожалению (но понятно) не указывает на “измерение” сложности, а только дает прагматический совет

в компиляторе, обрабатывающем ваш код, произошло переполнение стека. Чтобы решить эту проблему ошибка, упростите свой код.

чтобы подчеркнуть это еще раз: для частного случая глубоко вложенных for-петли, все это довольно академично и гипотетических. Но websearching для сообщения об ошибке CS1647 показывает несколько случаев, когда эта ошибка появилась для кода, который, скорее всего, не был преднамеренно сложным, но создавался в реалистичных сценариях.

Источник

Приветствуем вас на восьмом уроке по основам программирования. Мы продолжим углубляться в управление процессом выполнения программы и рассмотрим сегодня второй инструмент – циклы в программировании.

Читайте также:  Цикл для нарезания резьбы метчиком фанук

Циклы в программировании и их типы

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

Цикл в программировании состоит из трех частей:

  • 1. Оператор – название цикла.
  • 2. Условие – это то, при каких обстоятельствах цикл работает.
  • 3. Тело – код цикла, который должен сработать при прохождении условия.

Повторение цикла по-научному называется итерация.

Типы циклов в программировании:

  • 1. Безусловные (бесконечные) – пропущено условие.
  • 2. Условные.

Условные циклы в программировании, в свою очередь, подразделяются на

    • а) цикл с предусловием (while).

Цикл будет выполняться только пока истинно первоначальное условие. Проверка условия выполняется до выполнения цикла.

    • б) цикл с постусловием (do…while).

Проверка условия выполняется после выполнения тела цикла. Это говорит о том, что цикл выполняется хотя бы один раз.

    • в) цикл со счетчиком (for).

Этот цикл повторяет блок кода заданное число раз. В цикле задается некоторая переменная – счетчик, которая изменяет свое значение в некотором диапазоне. Счетчик после каждой итерации обновляется, а проверка условия выполняется перед выполнением тела цикла.

    • г) цикл просмотра (for in).

Этот цикл говорит о том, что будет выполняться операция Х для всех элементов, входящих во множество У. Используется для перечисления названий, индексов полей массивов либо объектов. Объект – это структура данных. Например, машина — объект, у которого есть определенные свойства (наличие 4 колес) и функции (ездить).

    • д) цикл с выходом из середины (break).

Это цикл, в котором есть команды, нарушающие порядок выполнения конструкций цикла. Существуют команда досрочного выхода из цикла – break и команда пропуска итерации – continue. Эти команды служат для управления работой цикла.

Бывают также вложенные циклы в программировании. Это значит, что циклы вложены один в другой.

Цикл со счетчиком является самым универсальным и используемым, поэтому рассмотрим подробно его создание и работу:

  • 1) инициализация счетчика;
  • 2) проверка выполнения условия;
  • 3) выполнение блока кода (тела цикла);
  • 4) обновление счетчика;
  • 5) переход к пункту 2.

Примеры циклов в программировании на Python

1.Цикл с предусловием

digit = 1
while digit <= 100 :
print (str (digit) + ‘ok!’)
digit = digit + 1
print (str (digit) + ‘more than 100’)

2.Цикл просмотра

arr = [1, 4, 6, 3, 10]
sum = 0
for i in arr :
sum += i
print(sum)

3.Цикл с выходом из середины

arr = [1, 4, 6, 3, 10]
sum = 0
for i in arr :
if i == 3 :
break / continue
sum += i
print(sum)

Примеры циклов в программировании на JavaScript

1.Цикл с предусловием

digit = 1;
while (digit <= 100) {
console.log(digit + ‘ok!’);
digit = digit + 1;
};
console.log(str (digit) + ‘more than 100’);

2.Цикл с постусловием

var digit = 10;
do {
console.log(‘ok!’);
digit = digit + 1;
} while (digit <= 10);
console.log(‘More than 10’);

3.Цикл со счетчиком

var arr = {1, 4, 6, 3, 10};
for(i = 0, sum = 0; i < arr.length; i++) {
sum += arr[i];
};
console.log(sum);

Читайте также:  Черная кровь в середине цикла причины

4.Цикл с выходом из середины

var arr = [1, 4, 6, 3, 10];
for(i = 0, sum = 0; i < arr.length; i++) {
if (arr[i] === 6) {
break / continue;
}
sum += arr[i];
};

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

Приятного всем просмотра! Учитесь с удовольствием! Всегда ваш Loftblog.

Рекомендуемые курсы

Источник

Это плохой вопрос интервью, если код транскрибируется точно.

class X
{
int i = 0;

Это обозначение недействительно C++. G++ говорит:

3:13: error: ISO C++ forbids initialization of member ‘i [-fpermissive]
3:13: error: making ‘i static [-fpermissive]
3:13: error: ISO C++ forbids in-class initialization of non-const static member ‘i

Мы проигнорируем это, предполагая, что код был написан как нечто более похожее:

class X
{
int i;
public:
X() : i(0) { }

Исходный код продолжается:

public:
Class *foo()
{
for ( ; i < 1000 ; ++i )
{
// some code but do not change value of i
}
return 0; // Added to remove undefined behaviour
}
}

Неясно, что такое Class * – тип Class не указан в примере.

int main()
{
X myX;
Thread t1 = create_thread( myX.foo() );

Поскольку здесь вызывается foo(), и его возвращаемое значение передается create_thread(), цикл будет выполняться здесь 1000 раз – не имеет значения, является ли это многоядерной системой. После выполнения циклов возвращаемое значение передается create_thread().

Поскольку у нас нет спецификации для create_thread(), невозможно предсказать, что он будет делать с Class * который возвращается из myX.foo(), больше, чем можно сказать, как myX.foo() фактически генерирует соответствующий Class * или то, что объект Class способен делать. Вероятность того, что нулевой указатель вызовет проблемы, однако, для вопроса, мы предположим, что Class * действителен и создается новый поток и помещается в режим ожидания, ожидая, когда операция “start” позволит ему запустить.

Thread t2 = create_thread( myX.foo() );

Здесь мы должны сделать некоторые предположения. Мы можем предположить, что Class * возвращаемый myX.foo(), не дает доступа к переменной-члену i которая находится в myX. Поэтому, даже если поток t1 работает до создания t2, нет никаких помех от t1 в значении myX, и когда основной поток выполняет этот оператор, цикл будет выполняться еще 0 раз. Результат myX.foo() будет использоваться для создания потока t2, который больше не может мешать i в myX. Мы обсудим варианты этих предположений ниже.

Start t1 …
Start t2 …

Потокам разрешено работать; они делают то, что подразумевается Class * возвращаемым из myX.foo(). Но нити не могут ссылаться и (а) изменять myX; им не был предоставлен доступ к нему, если Class * каким-то образом не обеспечивает этот доступ.

join(t1)
joint(t2)

Потоки завершены…

}

Таким образом, тело цикла выполняется 1000 раз до создания t1 и выполняется дополнительно 0 раз до создания t2. И неважно, является ли это одноядерной или многоядерной машиной.

Действительно, даже если вы предполагаете, что Class * предоставляет поток доступ к i и вы предполагаете, что t1 запускается немедленно (возможно, до того, как create_thread() вернется в основной поток), если он не изменяет i, поведение гарантированно “1000 и 0 раз”.

Ясно, что если t1 запускается при create_thread() и изменяет i в myX, то поведение неопределенно. Тем не менее, хотя потоки в приостановленной анимации до тех пор, пока не начнутся операции, нет неопределенности, и “1000 и 0 раз” остается правильным ответом.

Альтернативный сценарий

Если вызовы create_thread() не были учтены и код был:

Thread t1 = create_thread(myX.foo);
Thread t2 = create_thread(myX.foo);

где указатель на функцию-член передается create_thread(), тогда ответ совершенно другой. Теперь функция не выполняется до тех пор, пока потоки не будут запущены, и ответ будет неопределенным, есть ли один процессор или несколько процессоров на машине. Это сводится к проблемам планирования потоков, а также зависит от того, как оптимизирован код. Почти любой ответ от 1000 до 2000 правдоподобен.

При достаточно странных обстоятельствах ответ может даже быть больше. Например, предположим, что t2 выполнил и прочитал i как 0, затем был приостановлен, чтобы запустить t1; t1 обрабатывает итерации 0..900, а затем записывает i и передает управление t2, что увеличивает его внутреннюю копию i до 1 и записывает ее обратно, затем приостанавливается, а t1 запускается снова и считывает i и работает от 1 до 900 снова, а затем давайте t2 еще раз пойти… и т.д. В соответствии с этим неправдоподобным сценарием (неправдоподобный код для выполнения t1 и t2, вероятно, один и тот же, хотя все зависит от того, что действительно есть Class *), может быть много итераций.

Источник

Большая часть времени исполнения программы приходится на циклы: это могут быть вычисления, прием и обработка информации и т.д. Правильное применение техник оптимизации циклов позволит увеличить скорость работы программы. Но прежде, чем приступать к оптимизациям необходимо выделить «узкие» места программы и попытаться найти причины падения быстродействия.

Время исполнения кода в циклах зависит от организации памяти, архитектуры процессора, в том числе, поддерживаемого набора инструкций, конвейеров, кэшей и опыта программиста.
Рассмотрим некоторые методы оптимизаций циклов: развертка циклов (loop unrolling), объединение циклов (loop fusion), разрезание циклов (loop distribution), выравнивание циклов (loop alignment), перестановка циклов (loop interchange), разделение на блоки (loop blocking).
Перед применением какой-либо оптимизации сделайте самое простое: вынесите из цикла все переменные, которые в нем не изменяются.

Читайте также:  Что такое рабочая часть цикла
Какие причины могут привести к уменьшению скорости работы программы в циклах?
  1. Итерации цикла зависимы и не могут исполняться параллельно.
  2. Тело цикла большое и требуется слишком много регистров.
  3. Тело цикла или количество итераций мало и выгоднее совсем отказаться от использования цикла.
  4. Цикл содержит вызовы функций и процедур из сторонних библиотек.
  5. Цикл интенсивно использует какое-то одно исполняющее устройство процессора.
  6. В цикле имеются условные переходы.
Развертка циклов

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

ДоПослеПосле №2
for (int i = 0; i < iN; i++){
 res *= a[i];
}
for (int i = 0; i < iN; i+=3){
 res *= a[i];
 res *= a[i+1];
 res *= a[i+2];
}
for (int i = 0; i < iN; i+=3){
 res1 *= a[i];
 res2 *= a[i+1];
 res3 *= a[i+2];
}
res = res1 * res2 * res3;

В gcc можно применить следующие ключи: -funroll-all-loops -funroll-loops.

Объединение циклов

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

ДоПосле
for(int i = 0; i < iN; i++){
 a[i] = b[i] – 5;
}
for(int i = 0; i < iN-1; i++){
 d[i] = e[i] * 3;
}
for(int i = 0; i < iN-1; i++){
 a[i] = b[i] – 5;
 d[i] = e[i] * 3;
}
a[iN-1] = b[iN-1] – 5;

Разрезание циклов

Данная оптимизация применяется, когда тело цикла большое и переменным не хватает регистров. Поэтому данные сначала вытесняются в кэш, а если совсем все плохо, то и в оперативную память. А доступ к оперативной памяти занимает ~300 тактов процессора, а доступ к L2 всего ~10. Доступ к памяти с большим шагом еще больше замедляет программу. Оптимально «ходить» по памяти с шагом 2n, где n — достаточно маленькое число (<7).

ДоПосле
for (int j = 0; j < jN; j++){
for (int k = 0; k < kN; k++){
for (int m = 0; m < mN; m++){
 i = j * k + m;
 a[i] = b[i] * c[i] + f[i]/e[i]+ x[i] – y[i] +
  z[i]/r[i] + d[i] * x[i];
}
}
}
for (int j = 0; j < jN; j++){
for (int k = 0; k < kN; k++){
 double tmp;
 for (int m = 0; m < mN; m++){
  i = j * k + m;
  tmp = b[i] * c[i] + f[i]/e[i];
  a[i] = tmp – y[i] + z[i]/r[i] +
  (d[i] + 1) * x[i];
 }
}
}

Перестановка циклов

Во вложенных циклах важен порядок вложения. Поэтому необходимо помнить как хранятся массивы в памяти. Классический пример: c/c++ хранят матрицы построчно, а fortran — по столбцам.

ДоПосле
for(int i = 0; i < iN; i++){
for(int j = 0; j < jN; j++){
for(int k = 0; k < kN; k++){
 c[i][j] = c[i][j] + a[i][k] * b[k][j];
}
}
}
for(int i = 0; i < iN; i++){
for(int k = 0; k < kN; k++){
for(int j = 0; j < jN; j++){
  c[i][j] = c[i][j] + a[i][k] * b[k][j];
}
}
}

Теперь обращения к массиву a идут последовательно.

Разделение циклов на блоки

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

ДоПосле
for(int i = 0; i < iN; i++){
for(int j = 0; j < jN; j++){
 a[i][j] = a[i][j] + b[i][j];
}
}
// размер блоков зависит от размера исходных массивов
int iBlk, jBlk;
for(int k = 0; k < iN/iBlk; k++){
for(int m = 0; m < jN/jBlk; m++){
 for(int i = k * iBlk; i < ((k + 1) * iBlk); i++){
 for(int j = m * jBlk; j < ((m + 1) * jBlk); j++){
  a[i][j] = a[i][j] + b[i][j];
 }
 }
}
}

Примерно по такому принципу работает технология MPI: делит большие массивы на блоки и рассылает отдельным процессорам.

Разрешение зависимостей

Лучшее решение — избавиться. Но не со всеми зависимостями это получится.

for (int i = 1; i < N; i++){
 a[i] = a[i-1] + 1;
}

Для этого примера лучше применить развертку, т.к. результат вычислений будет оставаться на регистрах. Но большинство таких циклов не могут быть полностью оптимизированы (или распараллелены), результат все равно зависит от предыдущего витка цикла.
Чтобы проверить цикл на независимость, измените направление движения в цикле на обратное. Если результат вычислений не изменился, то итерации цикла — независимы.

Относительно условных переходов

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

Вместо заключения

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

  1. Ознакомтесь с архитектурой процессора (узнайте сколько и каких исполняющих устройств у него есть, сколько конвейеров, размеры кэшей L1 и L2).
  2. Попробуйте компилировать программу разными компиляторами и с различными ключами.
  3. Учитывайте влияние операционной системы.

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

Если хотите сами потренироваться в оптимизации, то попробуйте вычислить число Пи:

Ниже приведен «плохой» код.

long N = 10000000;
double dx, sum, x;
sum = 0.0;
x = 0.0;
dx = 1.0 / (double) N;
for (long i = 0; i < N; i++){
 sum += 4.0 / (1.0 + x * x);
 x += dx;
}
double pi = dx * sum;

О чем я не рассказал: о векторизации вычислений (инструкции SSE); о prefetch’ах, облегчающих работу с памятью. Если будут люди «которым интересно», то напишу отдельную статью про векторизацию циклов.

Подсветка исходных кодов Source Code Highlighter.

Источник