stringtranslate.com

Потоковый код

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

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

Поточный код наиболее известен по его использованию во многих компиляторах языков программирования , таких как Forth , многих реализациях BASIC , некоторых реализациях COBOL , ранних версиях B , [2] и других языках для небольших мини-компьютеров и для любительских радиоспутников . [ требуется ссылка ]

История

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

Ранние компьютеры имели относительно небольшой объем памяти. Например, большинство Data General Nova , IBM 1130 и многие из первых микрокомпьютеров имели всего 4 КБ оперативной памяти. Следовательно, много времени тратилось на поиски способов уменьшить размер программы, чтобы она поместилась в доступную память.

Одним из решений является использование интерпретатора, который читает символьный язык по частям и вызывает функции для выполнения действий. Поскольку исходный код обычно намного плотнее, чем полученный машинный код, это может сократить общее использование памяти. Вот почему Microsoft BASIC является интерпретатором: [a] его собственный код должен был делить 4 КБ памяти машин, таких как Altair 8800, с исходным кодом пользователя. Компилятор транслирует из исходного языка в машинный код, поэтому компилятор, исходный код и вывод должны находиться в памяти одновременно. В интерпретаторе нет вывода.

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

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

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

В 1970-х годах разработчики оборудования приложили значительные усилия, чтобы сделать вызовы подпрограмм более быстрыми и простыми. В улучшенных конструкциях для вызова подпрограммы тратится только одна инструкция, поэтому использование псевдоинструкции не экономит место. [ требуется цитата ] Кроме того, производительность этих вызовов почти не требует дополнительных накладных расходов. Сегодня, хотя почти все языки программирования сосредоточены на изоляции кода в подпрограммы, они делают это для ясности кода и удобства его обслуживания, а не для экономии места.

Системы потокового кода экономят место, заменяя этот список вызовов функций, где от одного вызова к другому меняется только адрес подпрограммы, списком токенов выполнения, которые по сути являются вызовами функций с удаленными кодами операций вызова, оставляя только список адресов. [3] [4] [5] [6] [7]

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

Разработка

Для экономии места программисты сжали списки вызовов подпрограмм в простые списки адресов подпрограмм и использовали небольшой цикл для вызова каждой подпрограммы по очереди. Например, следующий псевдокод использует эту технику для сложения двух чисел A и B. В этом примере список помечен как thread , а переменная ip (Instruction Pointer) отслеживает наше место в списке. Другая переменная sp (Stack Pointer) содержит адрес в другом месте памяти, который доступен для временного хранения значения.

start : ip = & thread // указывает на адрес '&pushA', а не на текстовую метку 'thread' top : jump * ip ++ // перейти по ip к адресу в потоке, перейти по этому адресу к подпрограмме, перейти по ip thread : & pushA & pushB & add ... pushA : * sp ++ = A // перейти по sp к доступной памяти, сохранить A там, перейти по sp к следующему переходу top pushB : * sp ++ = B jump top add : addend1 = *-- sp // Извлечь верхнее значение из стека addend2 = *-- sp // Извлечь второе значение из стека * sp ++ = addend1 + addend2 // Сложить два значения и сохранить результат наверху стека jump top                                      


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

start : ip = & thread // ip указывает на &pushA (которая указывает на первую инструкцию pushA) jump * ip ++ // передать управление первой инструкции pushA и переместить ip в &pushB thread : & pushA & pushB & add ... pushA : * sp ++ = A // перейти по sp в доступную память, сохранить A там, переместить sp в следующий jump * ip ++ // отправить управление туда, куда указывает ip (т. е. в pushB), и переместить ip pushB : * sp ++ = B jump * ip ++ add : addend1 = *-- sp // Извлечь верхнее значение из стека addend2 = *-- sp // Извлечь второе значение из стека * sp ++ = addend1 + addend2 // Сложить два значения и сохранить результат в верхней части стека jump * ip ++                                       

Это называется прямым нитевым кодом (DTC). Хотя эта техника и старше, первое широко распространенное использование термина «нитевой код» относится, вероятно, к статье Джеймса Р. Белла 1973 года «Нитевой код». [8]

В 1970 году Чарльз Х. Мур изобрел более компактную компоновку, косвенный потоковый код (ITC), для своей виртуальной машины Forth. Мур пришел к этой компоновке, потому что миникомпьютеры Nova имели бит косвенности в каждом адресе, что делало ITC простым и быстрым. Позже он сказал, что нашел это настолько удобным, что распространил его на все более поздние разработки Forth. [9]

Сегодня некоторые компиляторы Forth генерируют код с прямым потоком, в то время как другие генерируют код с косвенным потоком. Исполняемые файлы действуют одинаково в обоих случаях.

Модели резьбы

Практически весь исполняемый потоковый код использует тот или иной из этих методов для вызова подпрограмм (каждый метод называется «моделью потоков»).

Прямая заправка

Адреса в потоке — это адреса машинного языка. Эта форма проста, но может иметь накладные расходы, поскольку поток состоит только из машинных адресов, поэтому все дальнейшие параметры должны быть загружены косвенно из памяти. Некоторые системы Forth производят код с прямым потоком. На многих машинах прямой поток быстрее, чем поток подпрограмм (см. ссылку ниже).

Пример стековой машины может выполнять последовательность "push A, push B, add". Это может быть преобразовано в следующий поток и процедуры, где ipинициализируется по указанному адресу thread(т. е. адресу, где &pushAхранится).

#define PUSH(x) (*sp++ = (x)) #define POP() (*--sp) start : ip = & thread // ip указывает на &pushA (который указывает на первую инструкцию pushA) jump * ip ++ // передать управление первой инструкции pushA и переместить ip в &pushB thread : & pushA & pushB & add ... pushA : PUSH ( A ) jump * ip ++ // передать управление туда, куда указывает ip (т. е. в pushB), и переместить ip pushB : PUSH ( B ) jump * ip ++ add : result = POP () + POP () PUSH ( result ) jump * ip ++                          

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

#define PUSH(x) (*sp++ = (x)) #define POP() (*--sp) start : ip = & thread jump * ip ++ thread : & push & A // адрес, где хранится A, а не литерал A & push & B & add ... push : variable_address = * ip ++ // необходимо переместить ip за адрес операнда, так как это не адрес подпрограммы PUSH ( * variable_address ) // Считать значение из переменной и поместить в стек jump * ip ++ add : result = POP () + POP () PUSH ( result ) jump * ip ++                            

Непрямая резьба

Косвенная потоковая обработка использует указатели на местоположения, которые в свою очередь указывают на машинный код. Косвенный указатель может сопровождаться операндами, которые хранятся в косвенном «блоке», а не сохраняются повторно в потоке. Таким образом, косвенный код часто более компактен, чем код с прямой потоковой обработкой. Косвенная обработка обычно делает его медленнее, хотя обычно все еще быстрее, чем интерпретаторы байт-кода. Когда операнды обработчика включают как значения, так и типы, экономия места по сравнению с кодом с прямой потоковой обработкой может быть значительной. Старые системы FORTH обычно создают код с косвенной потоковой обработкой.

Например, если цель — выполнить «push A, push B, add», можно использовать следующее. Здесь ipинициализируется адресом &thread, каждый фрагмент кода ( push, add) находится путем двойной косвенной переадресации через ipи косвенный блок; и любые операнды фрагмента находятся в косвенном блоке, следующем за адресом фрагмента. Это требует сохранения текущей подпрограммы в ip, в отличие от всех предыдущих примеров, где она содержала следующую подпрограмму для вызова.

start : ip = & thread // указывает на '&i_pushA' jump * ( * ip ) // следовать указателям на 1-ю инструкцию 'push', НЕ продвигать ip пока thread : & i_pushA & i_pushB & i_add ... i_pushA : & push & A i_pushB : & push & B i_add : & add push : * sp ++ = * ( * ip + 1 ) // искать 1 после начала косвенного блока для адреса операнда jump * ( *++ ip ) // продвигать ip в потоке, прыгать через следующий косвенный блок к следующей подпрограмме add : addend1 = *-- sp addend2 = *-- sp * sp ++ = addend1 + addend2 jump * ( *++ ip )                                      

Подпрограмма потоковой передачи

Так называемый «код с потоками подпрограмм» (также «код с потоками вызовов») состоит из серии инструкций «вызова» на машинном языке (или адресов функций для «вызова», в отличие от использования «перехода» в прямой потоковой обработке). Ранние компиляторы для ALGOL , Fortran, Cobol и некоторых систем Forth часто создавали код с потоками подпрограмм. Код во многих из этих систем работал на стеке операндов «последним пришел — первым вышел» (LIFO), для которого была хорошо развита теория компиляторов. Большинство современных процессоров имеют специальную аппаратную поддержку для инструкций «вызова» и «возврата» подпрограмм, поэтому накладные расходы на одну дополнительную машинную инструкцию на отправку несколько уменьшаются.

Антон Эртл, один из создателей компилятора Gforth , заявил, что «вопреки популярным мифам, потоки подпрограмм обычно медленнее прямых потоков». [10] Однако последние тесты Эртла [1] показывают, что потоки подпрограмм быстрее прямых потоков в 15 из 25 тестовых случаев. Более конкретно, он обнаружил, что прямые потоки являются самой быстрой моделью потоков на процессорах Xeon, Opteron и Athlon, косвенные потоки являются самыми быстрыми на процессорах Pentium M, а потоки подпрограмм являются самыми быстрыми на процессорах Pentium 4, Pentium III и PPC.

В качестве примера потокового вызова для «push A, push B, add»:

поток : вызов pushA вызов pushB вызов add ret pushA : * sp ++ = A ret pushB : * sp ++ = B ret add : addend1 = *-- sp addend2 = *-- sp * sp ++ = addend1 + addend2 ret                           

Протягивание токенов

Код с токен-потоками реализует поток как список индексов в таблице операций; ширина индекса, естественно, выбирается как можно меньше для плотности и эффективности. 1 байт / 8 бит является естественным выбором для простоты программирования, но меньшие размеры, такие как 4 бита, или большие, такие как 12 или 16 бит, могут использоваться в зависимости от количества поддерживаемых операций. Пока ширина индекса выбирается уже, чем машинный указатель, она, естественно, будет более компактной, чем другие типы потоков, без особых усилий со стороны программиста. Обычно она составляет от половины до трех четвертей размера других потоков, которые сами по себе составляют от четверти до одной восьмой размера непоточного кода. Указатели таблицы могут быть как косвенными, так и прямыми. Некоторые компиляторы Forth создают код с токен-потоками. Некоторые программисты считают, что « p-код », генерируемый некоторыми компиляторами Pascal , а также байт-коды, используемые в .NET , Java , BASIC и некоторых компиляторах C , являются токен-потоками.

Распространенным подходом, исторически, является байт-код , который обычно использует 8-битные коды операций с виртуальной машиной на основе стека. Архетипический интерпретатор байт-кода известен как «интерпретатор декодирования и отправки» и имеет следующую форму:

start : vpc = & thread dispatch : addr = decode ( & vpc ) // Преобразуем следующую операцию байт-кода в указатель на машинный код, который ее реализует // Здесь выполняются любые операции между инструкциями (например, обновление глобального состояния, обработка событий и т. д.) jump addr CODE_PTR decode ( BYTE_CODE ** p ) { // В более сложной кодировке может быть несколько таблиц для выбора или флагов управления/режима return table [ * ( * p ) ++ ]; } thread : /* Содержит байт-код, а не машинные адреса. Поэтому он более компактен. */ 1 /*pushA*/ 2 /*pushB*/ 0 /*add*/ table : & add /* table[0] = адрес машинного кода, реализующего байт-код 0 */ & pushA /* table[1] ... */ & pushB /* table[2] ... */ pushA : * sp ++ = A jump dispatch pushB : * sp ++ = B jump dispatch add : addend1 = *-- sp addend2 = *-- sp * sp ++ = addend1 + addend2 jump dispatch                                                    

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

Для инструкций, где отдельные операции просты, например, «push» и «add», накладные расходы , связанные с принятием решения о том, что выполнять, больше, чем стоимость фактического выполнения, поэтому такие интерпретаторы часто намного медленнее машинного кода. Однако для более сложных («составных») инструкций процент накладных расходов пропорционально менее значителен.

Бывают случаи, когда токен-потоковый код может иногда работать быстрее, чем эквивалентный машинный код, когда этот машинный код оказывается слишком большим, чтобы поместиться в кэш инструкций L1 физического процессора. Более высокая плотность кода потокового кода, особенно токен-потокового кода, может позволить ему полностью поместиться в кэш L1, когда в противном случае он бы этого не сделал, тем самым избегая пробуксовки кэша. Однако потоковый код потребляет как кэш инструкций (для реализации каждой операции), так и кэш данных (для байт-кода и таблиц) в отличие от машинного кода, который потребляет только кэш инструкций; это означает, что потоковый код будет съедать бюджет для объема данных, которые могут быть сохранены для обработки процессором в любой момент времени. В любом случае, если вычисляемая проблема подразумевает применение большого количества операций к небольшому объему данных, то использование потокового кода может быть идеальной оптимизацией. [4]

Нить Хаффмана

Поточный код Хаффмана состоит из списков токенов, хранящихся как коды Хаффмана . Код Хаффмана представляет собой строку битов переменной длины, которая идентифицирует уникальный токен. Поточный интерпретатор Хаффмана находит подпрограммы с помощью индексной таблицы или дерева указателей, по которым можно перемещаться с помощью кода Хаффмана. Поточный код Хаффмана является одним из самых компактных представлений, известных для компьютерной программы. Индекс и коды выбираются путем измерения частоты вызовов каждой подпрограммы в коде. Частым вызовам присваиваются самые короткие коды. Операциям с примерно одинаковой частотой присваиваются коды с почти одинаковой длиной бит. Большинство систем с потоками Хаффмана были реализованы как системы Forth с прямым потоком и использовались для упаковки больших объемов медленно работающего кода в небольшие дешевые микроконтроллеры . Большинство опубликованных [11] применений были в смарт-картах, игрушках, калькуляторах и часах. Бит-ориентированный токенизированный код, используемый в PBASIC, можно рассматривать как разновидность кода Хаффмана.

Менее используемая резьба

Примером является потоковое программирования строк, в котором операции идентифицируются строками, обычно искомыми хэш-таблицей. Это использовалось в самых ранних реализациях Forth Чарльза Х. Мура и в экспериментальном аппаратно-интерпретируемом компьютерном языке Иллинойсского университета . Это также используется в Bashforth.

РПЛ

RPL от HP , впервые представленный в калькуляторе HP-18C в 1986 году, представляет собой тип фирменного гибридного (прямо-поточного и косвенно-поточного) потокового интерпретируемого языка (TIL) [12] , который, в отличие от других TIL, позволяет встраивать «объекты» RPL в «поток выполнения», т. е. поток адресов, по которым перемещается указатель интерпретатора. «Объект» RPL можно рассматривать как специальный тип данных, чья структура в памяти содержит адрес «пролога объекта» в начале объекта, а затем следуют данные или исполняемый код. Пролог объекта определяет, как должно выполняться или обрабатываться тело объекта. Используя «внутренний цикл RPL», [13], который был изобретен и запатентован [14] Уильямом К. Уикесом в 1986 году и опубликован в 1988 году, выполнение происходит следующим образом: [15]

  1. Разыменовываем IP (указатель инструкции) и сохраняем его в O (указатель текущего объекта)
  2. Увеличить IP на длину одного указателя адреса
  3. Разыменовать O и сохранить его адрес в O_1 (это второй уровень косвенности)
  4. Передать управление следующему указателю или встроенному объекту, установив PC (счетчик программ) на O_1 плюс один адресный указатель
  5. Вернуться к шагу 1

Более точно это можно представить следующим образом:

 О = [Я] Я = Я + Δ ПК = [О] + Δ

Где выше O — текущий указатель объекта, I — указатель интерпретатора, Δ — длина одного адресного слова, а оператор «[]» обозначает «разыменование».

Когда управление передается указателю объекта или внедренному объекту, выполнение продолжается следующим образом:

PROLOG -> PROLOG (адрес пролога в начале кода пролога указывает на самого себя) ЕСЛИ О + Δ =/= ПК ЗАТЕМ ПЕРЕЙТИ К НЕПРЯМОМУ (Тест на прямое выполнение) O = I - Δ (исправьте O, чтобы он указывал на начало внедренного объекта) I = I + α (исправьте I, чтобы он указывал после внедренного объекта, где α — длина объекта) КОСВЕННЫЙ (Остальная часть пролога)

В микропроцессорах HP Saturn , использующих RPL, есть третий уровень косвенности, который стал возможен благодаря архитектурному/программному трюку, обеспечивающему более быстрое выполнение. [13]

Филиалы

Во всех интерпретаторах ветвь просто изменяет указатель потока ( ip) на другой адрес в потоке. Условный переход-если-ноль, который переходит только если значение вершины стека равно нулю, может быть реализован, как показано ниже. В этом примере используется встроенная версия параметра прямой потоковой передачи, поэтому строка &thread[123]является местом назначения, куда следует перейти, если условие истинно, поэтому ее необходимо пропустить ( ip++), если ветвь не выполняется.

thread : ... & brz & thread [ 123 ] ... brz : when_true_ip = * ip ++ // Получить адрес назначения для ветви if ( *-- sp == 0 ) // Извлечь/использовать вершину стека и проверить, равна ли она нулю ip = when_true_ip jump * ip ++                  

Общие удобства

Разделение стеков данных и возвратов в машине устраняет большую часть кода управления стеком, существенно уменьшая размер потокового кода. Принцип двойного стека возникал трижды независимо: для больших систем Burroughs , Forth и PostScript . Он используется в некоторых виртуальных машинах Java .

Три регистра часто присутствуют в потоковой виртуальной машине. Еще один существует для передачи данных между подпрограммами («словами»). Это:

Часто потоковые виртуальные машины , такие как реализации Forth, имеют в основе простую виртуальную машину, состоящую из трех примитивов . Это:

  1. гнездо , также называемое докол
  2. unnest или semi_s (;s)
  3. следующий

В виртуальной машине с косвенным потоком, представленной здесь, операции следующие:

 следующий : * ip ++ -> w прыжок ** w ++ вставка : ip -> * rp ++ w -> ip следующий распаковка : *-- rp -> ip следующий                  

Смотрите также

Примечания

  1. ^ Dartmouth BASIC , на котором в конечном итоге основан Microsoft BASIC , был компилятором, работавшим на мэйнфреймах.

Ссылки

  1. ^ ab "Скорость различных методов диспетчеризации интерпретатора V2".
  2. ^ Деннис М. Ритчи, «Развитие языка C», 1993. Цитата: «Компилятор B на PDP-7 не генерировал машинные инструкции, а вместо этого «потоковый код»…»
  3. ^ Дэвид Фреч. "muforth readme". раздел "Простой и рекурсивный собственный компилятор".
  4. ^ ab Стив Хеллер. «Эффективное программирование на C/C++: меньше, быстрее, лучше». 2014. Глава 5: «Вам нужен интерпретатор?» стр. 195.
  5. ^ Жан-Поль Трембле; PG Sorenson. «Теория и практика написания компиляторов». 1985. стр. 527
  6. ^ «Беспроводной мир: электроника, радио, телевидение, том 89». стр. 73.
  7. ^ "Байт, Том 5". 1980. С. 212
  8. ^ Белл, Джеймс Р. (1973). «Поточный код». Сообщения ACM . 16 (6): 370–372. doi : 10.1145/362248.362270 . S2CID  19042952.
  9. Мур, Чарльз Х., опубликовал замечания в четвертом выпуске журнала Byte Magazine.
  10. ^ Эртл, Антон. «Что такое потоковый код?».
  11. ^ Латендресс, Марио; Фили, Марк. Генерация быстрых интерпретаторов для сжатого по Хаффману байт-кода . Elsevier. CiteSeerX 10.1.1.156.2546 . 
  12. ^ Loelinger, RG (1981) [август 1979]. Написано в Дейтоне, Огайо, США. Threaded Interpretive Languages: Their Design and Implementation (2nd printing, 1st ed.). Питерборо, Нью-Гемпшир, Великобритания: BYTE Books , BYTE Publications Inc. ISBN  0-07038360-X. LCCN  80-19392. ISBN 978-0-07038360-9 . Получено 03.08.2023(xiv+2+251 страницы)
  13. ^ ab Busby, Jonathan (2018-09-07). "The RPL inner loop explained". Музей калькуляторов HP . Архивировано из оригинала 2023-08-03 . Получено 2019-12-27 .
  14. ^ Wickes, William C. (1986-05-30). «Система обработки данных и метод для прямого и косвенного выполнения однородно структурированных типов объектов». uspto.gov . Получено 2019-12-27 .
  15. ^ Wickes, William C. (1988-10-01) [14–18 июня 1988 г.]. Forsely, Lawrence P. (ред.). RPL: ​​язык математического управления. Труды конференции Rochester Forth 1988 г.: среды программирования. Том 8. Рочестер, Нью-Йорк, США: Institute for Applied Forth Research, Inc., Университет Рочестера . ISBN 978-0-91459308-9. OCLC  839704944.(Примечание. Это название часто упоминается как «RPL: язык управления математикой». Отрывок доступен по адресу: RPLMan из файла Goodies Disk 4Zip)

Дальнейшее чтение

Внешние ссылки