Русский
Русский
English
Статистика
Реклама

Алгоритмы

Модель натурального числа. Часть I

25.06.2020 16:23:50 | Автор: admin



Теоремы существования и теоремы перечисления. Модели


Ограниченность научных, теоретических знаний тормозит научное и общественное развитие.
Взять закон распределения простых чисел (ЗРПЧ) или ту же основную теорему арифметики (ОТА). Да, они фундаментальны, но что ОТА утверждает?

Это всего лишь (как и ЗРПЧ) теорема существования, для любого числа N существует произведение степеней простых чисел, которое единственно и равно этому числу N. Как получить и увидеть отдельные делители N в теореме не говорится. Аналогично и с простыми числами. Сами числа (большие и очень большие) получать умеем только квазипростыми.

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

Более того, большинство и других основных теорем теории чисел теоремы существования. Чтобы воспользоваться многими теоремами относительно простых или составных чисел N необходимо располагать разложением этого числа на делители. А этого-то мы и не умеем!
Где же ответ? Где ключевые направления для получения положительных решений?

Материал публикации представляется весьма обширным, поэтому для удобства ознакомления с ним разбивается на две части.

Предварительные результаты, необходимые для разработки модели числа


Автором предлагаемой статьи длительное время самостоятельно разрабатываются теории НРЧ, отдельного числа и факторизации чисел в рамках нового (пока не было ссылок в комментариях о чем-то подобном в прошлом) оригинального (нет ссылок и на заимствования у других авторов) подхода к решению проблемы.

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

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

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

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

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

Известно крылатое выражение электрон неисчерпаем, могу здесь его переиначить Натуральный ряд чисел загадочен и неисчерпаем. Какой объект из двух менее изучен наукой сказать не берусь. Обнадеживают мои скромные усилия, те люди, которые (даже при наличии минусовых оценок на Хабре) в копиях публикуют на своих сайтах научные результаты моих публикаций и дают на них ссылки своим читателям.

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

В результате такой работы могут обнаружиться некоторые новые закономерности, то есть появляется возможность связать один ряд фактов с другим или сопоставить появление какого-то типа поведения с действием определенного ряда влияний. Такая связь в дальнейшем может быть сформулирована в виде обобщения или закона природы.

Например, публикация в 2010 г Арнольда В.И. Случайны ли квадратичные вычеты? обратила мое более пристальное внимание на этот объект, поскольку я был убежден в его неслучайности, детерминированности, а сомнения в этом академика РАН от математики для меня было просто удивительным.

В рамках фрагмента НРЧ, определяемого значением N, ряд при его формировании как бы самостоятельно определяет в какой позиции, что нужно вписать, где полный квадрат, а где другое значение. Более правильным вопросом относительно квадратичных вычетов (КВВ) мог бы быть: Почему они так распределены, почему так происходит?

Поиск причин конкретных распределений квадратичных вычетов в конечных числовых кольцах вычетов (КЧКВ) по составному модулю N, и в частности, квадратичных вычетов полных квадратов (КВК) привел к формулированию нового закона Распределения делителей числа в НРЧ.

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

Законом вскрывается причинно-следственная связь положения в НРЧ делителей, их кратных и квадратичных вычетов-полных квадратов по модулю N.

Все с делителями оказалось очень просто, даже простые числа не потребовались для их поиска. Но остается не вполне ясным, как такое управление распределением КВК происходит. Если натуральному ряду сообщить, т. е. задать значение составного модуля (N) кольца или простого числа (Р) для алгебраического поля, то он как бы автоматически укажет позиции, в которых квадратичный вычет становится квадратом. Выполняя редукцию, мы заранее результата не знаем, а квадрат возникает как раз там, где он и должен быть.

Долгое время оставалось загадкой, почему так происходит. Математики это видели всегда, но проходили мимо, не обращая внимания на сам факт, не удивлялись этому, не задавались вопросом и не искали разгадки. А академик РАН Арнольд В.И. вопрос себе задал, выполнил исследование и позднее опубликовал его.

Именно КВК несут в себе информацию о делителях числа N (выполняют роль обнаружителей, детекторов делителей), и они вычисляются элементарными операциями. Так был открыт новый закон в теории чисел. Он мог быть открыт и раньше, но публикация, надоумившая меня серьезно заняться этим вопросом случайно попалась мне на глаза лишь в 2014 году.
Закон распределения делителей числа обнаруживает их положение в НРЧ и обеспечивает их перечисление для заданного составного числа N.

Модель отдельного нечетного числа


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

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

В теории чисел значительное место отводится проблеме факторизации. Теорема факторизации устанавливает необходимость рассмотрения числового множества, ограниченного лишь подмножеством нечетных натуральных чисел (СННЧ) определенного вида (здесь), что является и достаточным для решения задачи факторизации. Это ограничение существенно упрощает проблему моделирования основного объекта (числа), но не делает ее простой.

Кроме модели числа важно располагать и более объемлющей моделью, в которой сами числа являются всего лишь отдельными элементами. Таким объемлющим числа объектом рассматривается НРЧ, модели которого также рассматриваются в предлагаемом новом подходе к проблеме факторизации. Это линейная модель (обычный НРЧ), плоскостные: контурная модель-спираль Улама (полная) и квадратичная Г2 модель (модель усеченного НРЧ). (здесь)

В этой публикации начнем рассмотрение моделей отдельного числа. В каждой из названных моделей НРЧ элементами выступают отдельные числа: в линейной это индекс позиции регистрового разряда, в котором помещается число, в контурной модели нечетное число полуконтур (левый или правый) с множеством свойств, в квадратичной Г2 модели число представляется разностью\суммой квадратов (целочисленными точками гиперблы, окружности) N=x12xo2.

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

Обоснование выбора списковой многострочной модели (СММ) числа


В статье предлагается концепция линейной алгебраической модели числа N, с погружением ее в множество подобных, представляемой многострочной таблицей. Модель достаточно простая. Будем представлять СННЧ N разбиением на две части N=x1+x0 всеми возможными способами.

Очевидно, таких способов представления числа N существует (N 1), при этом каждая сумма размещается в отдельной строке таблицы модели. Переменная x0 пробегает все строки от 1 до (N 1), т. е. играет роль текущего номера строки СМ-модели, который может совпадать в некоторой строке с конкретным делителем N и его кратными (см. табл.1).

Таблица 1 фрагмент СМ- модели


Обоснование выбора такой модели следующее. В линейной модели НРЧ присутствуют все числа без пропусков, что позволяет задать любое составное нечетное N = рq в соответствующей позиции. Очевидно, фрагмент НРЧ от 1 до N = рq, где р и q простые числа ( 2), содержит следующие подряд все числа, среди которых обязательно встретятся р и q и их кратные. Добавив к этому множеству чисел нулевой элемент получим конечное числовое кольцо вычетов (КЧКВ) по составному модулю N.

Если это множество чисел (фрагмент НРЧ) содержит среди элементов делители и кратные им значения, то возникает мысль, нельзя ли выудить из таких элементов сами делители р и q. Поставив целью исследования выуживание делителей из модели, модель должна сохранить полностью элементы фрагмента НРЧ.

Следовательно, простота, полнота, включение делителей и их кратных, структурность КЧКВ и гипотеза о возможности выуживания делителей положены в обоснование выбора СМ-модели.

Последовательность натуральных чисел СММ и ее фундаментальное свойство
Последовательность непрерывно следующих натуральных чисел используется в СМ-модели в двух колонках x1 и x0, в которых x0 играет роль текущего номера строки модели до середины исходного списка. Значение х1 = N хо дополнение до модуля снизу вверх записывается в строках модели. Для каждого из элементов (номера и дополнения) строки вычисляется rл(x0) и rл(x1) квадратичный вычет по модулю N. Эти КВВ совпадают, что допускает ограничиваться хо.

Для малых значений x0 редукция по N не меняет значений rл (x0) и строки, содержащие такие неизменяемые левые вычеты, всегда образуют для различных N область тривиальных КВК, обозначаемую ТКВК. Граница этой области называется левым верхним первым порогом, а ее значение вычисляется, например, для N=1961, как x0в1= N=1961=44,28, округляется до 44.

Полные квадраты из ТКВК области не используются ЗРД и не участвуют в факторизации. При движении вниз по строкам таблицы за пределами порога КВВ (x0>44) превышают модуль и редуцирование уменьшает значения. В некоторых строках при этом получаются дубли из ТКВК и они (и только они) используются для факторизации в рамках ЗРД .

Итак, последовательность содержит делители N и их кратные; начинается тривиальной ТКВК областью; некоторым значениям хо в дублях строк соответствуют КВВ полные квадраты.

Последовательность нечетных чисел СММ и ее фундаментальное свойство
В предлагаемой модели возникают последовательности нечетных чисел Т и Тп. Они играют существенную роль и на этом явлении остановимся подробнее. Ряды нечетных чисел (колонки 4 и 7) в таблице СММ встречно направлены, но состав их и структура идентичны
Т, Тп = {ti, i = 1(1)(N-1), ti =1(2) N-2}.

Они в НРЧ образуют бесконечную непрерывную последовательность нечетных значений, (но в примерах ограничены значениями от 1 до N 2). Каждый элемент t такого ряда (последовательности) будем представлять суммой смежных чисел t=t1+t0, где значение tо=t1-1. Слагаемые t1+t0 будем перемножать друг на друга р(t)=t1t0. Обозначим редуцированное произведение символом rc (t) =(t2 -1) и назовем средним вычетом. Оказывается, такие произведения обладают рядом замечательных свойств.

Во-первых, произведения таковы, что их флексии принимают только три четных значения {0, 2, 6}. Флексии в нечетных рядах следуют пятерками строк, которые следуют в порядке 02620 (табл. 1) и повторяются периодически до бесконечности.

Во-вторых, значения произведения р(t) с ростом ti возрастают, но в рамках модели редуцируются при превышении модуля N. Произведения меньшие модуля при редукциях остаются без изменений, и обладают свойством сохранять смежность сомножителей (ССС). Множество строк, в которых ti следуют подряд и их произведения обладают свойством ССС называется тривиальной областью и обозначается ТССС (заливка в колонке 6 зеленый цвет).

Граница этой области называется нижним средним первым порогом, а ее значение вычисляется как 1892 = 442 44, где значение 44=N=196144,2. В СММ (табл.1) в колонке 6 размещены строки ТССС средние вычеты rc(t) в строках с номерами хо = 937 по хо = 980 элементы.

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

Итак, последовательность нечетных чисел t завершается тривиальной ТССС областью (колонка 6); некоторым значениям хо в дублях строк этой области соответствуют КВВ-полные квадраты, что свидетельствует о непустом пересечении двух нетривиальных областей ТКВКТССС

Пример 1. (Редуцированные значения со свойством ССС). Пусть в СММ (см табл.1) задано
N = 1961 = рq и значение t=t1+t0 = 45 = 22 + 23. Находим значение произведения смежных слагаемых р(t) = 2223 = 506. Результат не превышает модуля 506 <1961, следовательно, редукция оставляет результат без изменения, т.е. rс(р) = р(mod N) = 506(mod 1961)= 506.
Флексия 506(mod10)=6 {0, 2, 6} говорит о выполнимости свойства rс(р) ССС.

Изменим t=t1+t0 = 89 = 44 +45. Находим значение произведения смежных слагаемых
р(t) = 4445 = 1980. Результат превышает модуль 1980 >1961, следовательно, требуется редукция, уменьшающая результат произведения, т.е. rс(р) = р(mod N) =1980(mod 1961)= 19.
Флексия результата 19(mod10)=9 {0, 2, 6} показывает, что свойство rс(р) ССС не выполняется.

Рассмотрим явления, происходящие при моделировании отдельного числа N, в рамках СМ-модели. Удобно это сделать на числовом примере, сопровождаемом комментариями. Выбор числа сделаем таким, чтобы исключить простое угадывание делителей. Здесь ограничимся лишь строками нижней части основной таблицы СМ-модели, хотя и не столь малого объема.

Пример 2. Задано число N = 1961 = рq, которое подвергнем факторизации. Для этого воспользуемся оригинальной СМ-моделью (см. табл.1) и определим (заполнены) полные верхнюю
rл(1) = 1, х1=1960, хо =1, t=1959, rс(1) = 491, tп = 1, rп(1) = N rл(1) = 1961 1 = 1960 и нижнюю
rл(0) = 1471, х1=981, хо =980, t=1, rс(0) = 0, tп = 1959, rп(0) = N rл(0) = 1961 1471 = 490, строки СММ а также остальные элементы в области ТССС и в других строках. Дело в том, что свойства модели допускают заполнение всех полей строк основной таблицы ручным способом, даже не прибегая к использованию компьютерной программы.

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

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

Теперь можно в деталях описать фрагмент модели числа (таблицы 1).
Описание фрагмента таблицы СМ-модели отдельного числа. Полная таблица строк СММ для N = 1961 содержит 980 строк и 8 столбцов (левый столбец примечаний не обязателен). Предварительно заполним поля столбцов t, tп, верхнюю и нижнюю строки СМ-модели. Строки нумеруются подряд сверху вниз хо до середины списка (нижняя нулевая строка р = rc=0) и далее продолжение х1 снизу вверх в колонке слева (слева от хо = 980 стоит х1 = 981).

Строки имеют типовую начинку: левый квадратичный вычет rл(хо), номер хо строки, х1 дополнение номера до N, разность t=х1хо пробегает монотонно убывающие нечетные значения от N 2 до 1, произведение р(t)=t1tо= (t2 1), средний вычет rс(р) = р(mod N), tп нечетные числа от 1 до N 2, правый вычет произведения rп(хо) = хох1 (mod N) = N rл(хо). Кратными будем называть строки, номера которых кратны делителям N.

Из полного списка строк приводимый фрагмент (табл.1) включает лишь 57 строк: первую, 901-ю, и все с 925-й по 980-ю строки. Заливкой выделены пары строк: номера хо = 901 и хо = 954 которых кратны большему делителю (оранжевый цвет); номера хо = 925 и хо = 962 которых кратны меньшему делителю (зеленый цвет); номера хо = 958 и хо = 966 строк, содержащих квадратичные вычеты КВК полные квадраты (синий цвет).

Следовательно, оранжевые и зеленые строки кратные. В таблице замкнутый интервал строк между ближайшей парой кратных строк разного цвета хо = 954 и хо = 962 содержит 9 строк, средняя с номером хоц = (954 + 962) = 958 строка.

В соответствии с законом распределения делителей числа в этой хоц точке КВВ должен быть полным квадратом удаленности (в числе строк) от нее кратных строк (точек) с разным цветом заливки. Действительно, строки хо = 954 (оранжевая) и хо = 962 (зеленая) удалены от центральной хоц = 958 обе на 4 позиции (строки) каждая.

Более поучительный случай (синяя) строка, имеющая номер хоц = 966. Поскольку вычисление КВВ в этой точке дает результат rл(хоц) = хоц (t)2(mod N) = 9662(mod1961) = 412 равный полному квадрату (412), то эта точка (по ЗРД) должна быть центром замкнутого интервала с нечетным числом строк в нем 83 = 241+ 1.

Удаленность (41 позиция) обеих границ интервала от центра. Действительно, такая пара строк (заливкой разного цвета) с номерами кратными делителям N, существует. Это строки с номерами хо = 925 (зеленая) и хо = 1007 (оранжевая). Удаленность границ равна разности
1007 966 = 966 925 = 41. Длина замкнутого интервала 83 = 241+ 1.

Наличие таких строк (даже одной синей из них) делает допустимым на основе ЗРД быстрое и успешное завершение ЗФБЧ. В соответствии с законом распределения делителей числа N в НРЧ в установленных строках определяются значения их текущих номеров. Это номера (точки хо) тех строк, в которых порождаются КВК.

Далее, следуя пунктам алгоритма (ниже) вычисления значений делителей р и q, находятся и сами делители. Вообще подряд все строки восстанавливать не обязательно, хотя и полезно для понимания и обучения. Важно уметь оперативно получать те из них, в которых КВВ полные квадраты.

Установление таких строк реализуется элементарными действиями над элементами СМ-модели. В приведенном фрагменте таблицы СММ такими строками являются строки с номерами хо = 958 и хо = 966 (выделены заливкой синего цвета).

Каким образом получаются окончательные решения, т. е. как вычисляются делители N?
На самом деле кратные делителям правая и левая границы Гп, Гл замкнутых симметричных относительно центров интервалов с нечетным числом строк неизвестны, так как неизвестны ни делители N, ни коэффициенты кратности номеров строк-границ.

В примере используется фрагмент СМ-модели, предоставляющий числовые данные, которые легко наблюдаются и анализируются. Собственно, такие таблицы (небольшого объема) формируются и используются для исследовательских целей и решения конкретных задач, для визуализации найденных законов, закономерностей, свойств, поиска и выявления скрытого потенциала модели и подхода к факторизации в целом, теоретического вывода математических и логических формул.

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

Поэтому предлагаемый алгоритм факторизации начинается с определения полных квадратов (КВК) в колонке квадратичных вычетов СМ-модели. Это можно сделать, вычисляя очередной номер строки, при движении в СМ-модели сверху вниз, либо снизу вверх. Путем перебора для каждого очередного номера строки вычислять КВВ и проверять, является ли он полным квадратом.

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

При установленных в примере значениях таких квадратов (КВВ 16 и 1681) и точек хоц= 958, хоц= 966, в которых они получены возможно (обратная задача) построение замкнутого интервала с определением его границ Гп, Гл = хоц (rл )= 958 4 = 962, 954, а также
Гп, Гл = хоц (rл )= 966 41 = 1007, 925.

Для окончательного получения значений делителей используется алгоритм Евклида наибольшего общего делителя НОД. Для первого случая, строка
хоц= 958 (синяя) и rл(хоц) =16 =42 имеем
di = НОД(N, Гi ) = НОД (1961, 962) = 37; di = НОД(N, Гi ) = НОД (1961, 954) = 53.

Для второго случая, строка с номером хоц = 966 (синяя) и КВК rл(хоц) =1681 = 412 имеем di = НОД(N, Гi ) = НОД (1961, 925) = 37; di = НОД(N, Гi ) = НОД (1961, 1007) = 53.

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

Продолжим обоснование полученных результатов

Поиск КВК. Свойства СМ-модели позволяют определять КВК, избегая тотального перебора строк, и, более того, возможно ограничиться просмотром минимального числа позиций. Сделаем важное допущение о том, что в пределах ТССС найдутся по меньшей мере две строки с КВК, причем, один из квадратичных вычетов существенно меньше другого. Это допущение не лишено обоснований, но сейчас и здесь их приводить не будем.

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

Другая особенность СМ-модели состоит в замечательном свойстве самой модели. Вне модели воспользоваться свойством не удается. Оказывается, левый квадратичный вычет, может вычисляться не только через квадратичное сравнение. Он может представляться суммой левого квадратичного вычета нижней строки модели rл(0) и текущего среднего вычета строки. Используется и это свойство КВВ в СМ-модели: rл(хоц) = rл(0) + rс(хоц) = 1471 + 506 = 1977.

Выбор среднего вычета 506 специально сделан таким, чтобы сумма его с rл(0) превысила N.
Поскольку найденное значение превышает модуль N = 1961, то его следует редуцировать, т. е. привести по N модулю rл(хоц) = rл(0) + rс(хоц) (mod1961) = 1977 1961 = 16 = 42. Редукция суммы при этом равна разности, что для rл(хоц) ожидаемо как малое значение (получили квадрат =16).

Если бы был получен не квадрат, то выполняется переход в следующую строку и действия повторяются до тех пор, пока не возникнет квадрат, а его возникновение гарантируется. Поясним откуда взялось значение rс(хоц) = 506? Это достаточно простой трюк.

Среди прочих отыскивается наименьший средний вычет rс(хоц), который при суммировании его с КВВ нижней строки модели превысил бы значение модуля N. Само значение rс(хоц) = 506 получено из t нечетного числа, rс(t) до определенной границы не зависит от N. Для некоторых значений t их отображение р(t) = rс(t) = t1tо обратимо, т. е. по значению rс(t) можно восстановить t = р(р(t))-1=р(rс(t))-1.

Далее, для любых N набор rс(хо) одинаков до определенного предела (порога, зависящего от N). Зная значение rс(t) = 506, найдем и значение t. Извлекаем квадратный корень и округляем до меньшего целого t1 = 506 = 22,49, тогда значение tо= t1+1 и t = t1 + tо.
Фактически получены значения tо = 23, t1 = 22 их сумма t = t1 + tо =22 + 23 = 45. По значению t можно восстановить все элементы строки, в которой оно лежит.

Поиск центра интервала. Нам требуется определить точку хоц центра замкнутого интервала, в которой КВВ полный квадрат. Еще одно свойство СМ-модели обеспечивает решение и этой задачи. Свойство реализуется достаточно простой формулой
хоц(t) = (N t) = (1961 45) = 958. Итак, центр найден хоц(t) = 958.

Проверим, действительно ли, в этой точке КВК rл(хоц) = 16.
rл(хоц) = хоц (t)2(mod N) = 9582(mod1961) = 917764(1961) = 42+ 4681961 = 16+ 0 = 16 =42.
Поиск границ интервала. При наличии центральной точки интервала и КВК в ней границы интервала находятся по зависимостям ЗРД Гп, Гл = хоц (rл(хоц)).

Алгоритм решения ЗФБЧ с учетом области ТКВК


Необходимость решения ЗФБЧ в большой мере диктуется и определяется требованиями информационной безопасности взаимодействия объектов и субъектов между собой при обмене сообщениями. Используются разнообразные алгоритмы, и эффективность части из них определяется стойкостью относительно проводимых нарушителем атак.

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

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

В алгоритме задается составное число N = рq = dmdб где р и q простые числа высокой разрядности. Для удобства изложения при описании воспользуемся числовым примером, в котором р и q простые числа не очень высокой разрядности, что позволит без больших усилий воспринимать текст и понимать (проверять) практически все результаты промежуточные и окончательные.

Задача 3. Пусть задано составное число N = рq = 2501, необходимо найти его делители р и q. Предварительно находим пороги ТКВК хов1 = N=2501= 50,0099, ТССС хон1=1201, rсн1(t) = 2450.

Алгоритм



1. Заполняется первая строка. Левый, средний и правый вычеты rл(1)=1, rс(1) = 626, rп(1)=2500.
х1 = 2500, хо = 1, t = N 2 = 2501 2 = 2499, tп = 1.
В последней строке СММ rл(0) = 1251 + 625 = 1876, смежные значения х1 + хо = N = 2501,
х1 = 1251, хо = 1250, разность t = х1 хо = 1, rс(t =1) = 0, tп = N 2, rп(0) = 625.

2. Определяем циклически в области ТКВК номер строки меньшего среднего вычета со свойством ССС (из перебора исключаются все строки до верхнего 1-го порога среднего вычета) хо = N=1876=43,3 квадрат этого значения (округление в меньшую сторону) дает для rс(хо) = 625 + 432 = 2474, это меньше 1-го порога равного 2501.

Повторяем цикл, увеличиваем еще на единицу номер строки хо = 43 +1 = 44.
Следующее значение rс(хо) = 625 + 442 2501= 2561 2501 = 60 превышает порог на 60. Значение rс(хо) не соответствует ССС. Увеличиваем еще на единицу номер строки хо = 45, тогда rс(хо) = 625 + 452 2501 = 2650 2501 = 149.

Значение rс(хо) превышает порог, но также не соответствует свойству ССС.
Увеличиваем еще на единицу номер строки хо = 46. Тогда средний вычет имеет свойство ССС rс(хо) = 625 + 462 2501= 2741 2501 = 240 = 1516.

3. Вычисляем значение разности t = x1 -xо (нечетного t в области ТССС) t = 15 + 16 = 31.
4. Определяем для этого t номер центральной строки хоц (t) = (N t) = (250131) = 1235.
5. Находим для хоц (t) квадратичный вычет центральной строки замкнутого интервала.
rл(хо)=хо (t)2(mod N) =12352(mod2501) = 462 КВК.

6. Вычисляем делители с использованием НОД
dб = НОД(N, 46+1235) = 61; dm = НОД(N, 1235 46) = 41.
7. Проверка N = рq = 4161 =2501. Делители N успешно найдены.

Задача может решаться иначе:

Известно (автору), что в области ТКВК для RSA-чисел появляются два средних вычета со свойством ССС вначале с большим (при меньшем хо) значением, и ниже его с меньшим (в примере это rс(хо) = 240). Начать поиск решения можно с большего rс(хо). Покажем на примере, как это происходит.

Задано составное число N = рq = 2501, необходимо найти его делители р и q. Предварительно находим пороги ТКВК хов1 = N=2501= 50,0099, ТССС хон1=1201, rсн1(t) = 2450.

Первая строка. Левый, средний и правый вычеты rл(1) = 1, rс(1) = 626, rп(1) = 2500.
х1 = 2500, хо = 1, t = N 2 = 2501 2 = 2499, tп = 1.
В последней строке Вычисляем сумму и разность смежных слагаемых N = 1250 +1251, rл(0) = 1251 + 625 = 1876, смежные значения х1 + хо = N = 2501, х1 = 1251, хо = 1250, разность
t = х1 хо = 1, rс(t =1) = 0, tп =N 2, rп(0) = 625.

В цикле по хо от хо = 1 вычисляем rс(хо) и проверяем его принадлежность области ТССС. Если rс(хо) принадлежит ТССС, то переходим к П.3 алгоритма, если не принадлежит, то увеличиваем значение хо = хо +1 и повторяем действия цикла.

На пятом шаге хо = 5 получаем значение t =N -2xо = 2501 -10 =2491 rс(хо =5) =(t2 1)(modN) =650. Флексия 650(mod10)=0 {0, 2, 6} говорит о том, что средний вычет может иметь свойство ССС. Устанавливаем из смежных ли чисел произведение равное 650. Извлекаем квадратный корень t0=650 =25,49, t1 =t0+1=25+1=26.
Находим произведение р= 2526= 650, редуцированный вычет принадлежит ТССС.
Вычисляем значение нечетного t в области ТССС t = 25 + 26 = 51.

Определяем для этого t номер строки хоц (t) = (N t) = (2501 51) = 1225.
Находим для хоц (t) квадратичный вычет (левый в СММ)
rл(хо) = хо (t)2(mod N) =12252(mod2501) = 52.

Вычисляем делители с использованием НОД
dб = НОД(N, 5+1235) = 61; dm = НОД(N, 1235 5) = 41.

Проверка N = рq = 4161 = 2501. Такая схема также приводит к успешному решению.
Подробнее..

Модель натурального числа II

25.06.2020 20:11:16 | Автор: admin



Структура конечного кольца вычетов по составному модулю



Формирование работоспособной модели числа оказалось достаточно сложным и объемным процессом, поэтому принято решение разбить его на 2 статьи.
Будем рассматривать натуральный ряд чисел (НРЧ) и считать, что его элементы размещены в ячейках регистра бесконечной длины, для каждого элемента хо которого вычисляется квадрат, помещаемый в ленту другого бесконечного регистра. Если же задать нечетное составное натуральное число (СННЧ) N, то лента с квадратами преобразуется.

В нее будут включены не просто квадраты (хо), а квадратичные вычеты (КВВ) чисел rл хо2 (mod N) по модулю этого составного числа N=dмdб. Для ячеек двух этих регистровых полос будет выполняться условие комплементарности. При этом мы ограничиваемся рассмотрением лишь фрагмента НРЧ.

Добавление к фрагменту слева нулевого элемента превращает его в конечное числовое кольцо вычетов (КЧКВ) по модулю N. Если квадратичный вычет получает значение полного квадрата, то КВВ обозначается КВК.
Ситуация с моделированием чисел и факторизацией как пишут Манин и Панчишкин близка к тупику или уже в тупике.

В этой статье на основе закона распределения делителей (ЗРД см.здесь ) числа рассматривается вопрос о структуре КЧКВ и о том, как неизвестные нам делители N управляют возникновением полных квадратов КВВ в списке квадратичных вычетов, которые доступны нашему наблюдению.


Тривиальные области СММ нечетного числа


Введем несколько определений.
Определение. Списочной многострочной моделью (СММ, СМ-моделью) числа называется таблица строк заданного вида (формата из 8 именованных полей: rл, х1, хо, t, p, rc , tп, rп). Ниже привожу расчетные формулы для переменных.

rлхо2(modN)x12(modN)-квадратичный вычет левый;
х1 = N xо дополнение хо до модуля;
хо текущий номер строки СММ;
t=х1 хо =t1 + tо разность слагаемых строки; раскладывается в сумму смежных;
p =t1tо произведение смежных слагаемых;
rс (t2 1)(mod N) =t1tо(modN); средний вычет в строке;
tпi = 2xоi 1; нечетное число; сумма номеров смежных строк;
rп х1хо(modN=N rл произведение слагаемых; правый вычет в строке.

Определение. (ТКВК) Тривиальной областью строк СМ-модели называется начальное множество строк списка, следующих подряд, содержащих в качестве rл (левого КВВ) монотонно возрастающие полные квадраты.

Определение. Границей ТКВК (левый 1-й верхний порог) называется наибольший КВК полный квадрат, не превышающий составного модуля N, например, для N =34999, граница ТКВК
rлmax N (mod N) = 34999 = 187.
Извлекаемые квадратные корни округляются до целых значений.

Определение. (ТССС) Тривиальной областью строк СМ-модели (внизу списка) называется множество строк конца списка, следующих снизу вверх подряд, содержащих монотонно возрастающие разности нечетных чисел
t = x1-xо от 1 до границы (порога), а в качестве вычетов rс (полные квадраты за вычетом первой степени) и обладающие свойством сохранять смежность сомножителей (ССС).

Определение. Границей ТССС (средний 1-й нижний порог) называется наибольший КВК без первой степени квадрата, например, для N =34999, граница ТССС
rсmax (хо2 -хо) (mod N) =1872 -187 = 34782.

Тип модели


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

К первому типу относятся все RSA-числа и многие другие, в которых значения делителей с одной стороны не являются близкими, а с другой не очень удаленными друг от друга на числовой оси НРЧ. Поясним сформулированные условия.

Числа N в СММ могут существенно различаться не только своими значениями, но и делителями. Сами делители также могут иметь различия (малые и большие значения) при близких значениях их произведений, т.е. N1 и N2 близки, а их делители N1 = 111129 = 12419 существенно различаются на 1129 11 = 1118 или N2 = 101123 = 12423 всего на 123 101 = 22 единицы.

Числа, включающие в качестве делителей квадраты, имеют свои особенности. Указанные различия проявляются специфически в СМ-моделях и всегда желательно учитывать такую специфику N.
Например, в канонической СМ-модели (N = a(a+2) близнецы) дублируемые КВК следуют смежными парами, сумма первых степеней которых постоянна и равна среднему числу (a + 1).

Пример. (Делители числа значительно удалены друг от друга). Пусть N = 311129 = 34999; rс(1) = 26250 = 3rл(0) = 38750, контур НРЧ 34999 +35001 = 70000 с номером kп =70000:8 = 8750, инвариант N равен kп/2 = 4375, отсюда интервал для N содержит слагаемых 31 и среднее слагаемое равно dб = 1129, меньшее число tпм= 1 + dб dм = 1+1129 31 =1099 и большее число интервала tпб= dб + dм 1= 1129 + 311 = 1159; действительно, 15(tпм+ tпб) + dб =152258 +1129 = 34999.

Инвариант числа определяется суммой номеров 15 контуров и половиной номера большего
275+276+277+278+279+280+281+282+283+284+285+286+287+288+289+290/2= 4375.

Границы интервала и контуров левая Гл(k = 275) = (2275 1)2 = 301401, для интервала правая Гп(k = 290) = (2290)2 = 336400. Разность границ Гп Гл = 336400 301401 = 34999 = N.
Граница ТКВК хомах = 187 = 631 + 1; хо (rc = 0) = 3952; первая точка
хо = 952 дубля с КВК(177) = 31329.
В этой точке ЗРД определяются делители: 952 177 = 775 = 3125; 952 + 177 = 1129.

Рассмотрим поведение КВВ (всего имеется 187 КВК), дублирующих (181 КВК) тривиальной области списка. Квадрат числа 31 и квадраты его кратных не дублируются, так как число является делителем N.
Распределение КВК по списку обладает определенной регулярностью, КВК группируются по 12 строк с интервалом между ними 31, и интервал между группами строк равен 797 либо 766 строк. Это следует из анализа таблиц. Существует 15 групп 1-15. В пределах групп возникают аттракторы, строки как бы притягивающие полные квадраты КВК.

Определение. Аттрактором (притягивающим квадраты элементом НРЧ) будем называть точку (клетку) НРЧ, соответствующую значению кратному dб.

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

Для более ясного представления и понимания как все устроено с делителями N в НРЧ вначале будем рассматривать картину хорошо разделяемых областей аттракторов. Текст будем сопровождать числовым примером, в котором роль модуля играет полупростое число
N =311129 = 34999. Следовательно, аттракторами будут числа: 11129, 21129 = 2258, 31129 =3387, , 151129.


Клетка, выделенная заливкой зеленого цвета инволюция КЧКВ. Желтым цветом выделены КВК для каждого аттрактора. Соединения полосы переменной хо (без заливки) не везде показаны (не загромождают рисунок), но фактически имеют место, являются сплошной лентой.

Пока хо малы (левая желтая колонка верхней части таблицы) их квадраты 187 штук (хо2 < N) не превышают модуль N. Таким образом, начальный отрезок строк CM-модели для (хо) в НРЧ от 1 до N, содержащих КВВ равные подряд следующим полным квадратам (КВК) мы определили как область тривиальных квадратичных вычетов (rл) квадратов (ТКВК).

Эти значения при дальнейшем продолжении вычислений КВВ будут изредка повторно встречаться (дублироваться) еще по одному разу (при полупростом N) в других точках хо или более при произвольном N.

Рассмотрение для подряд следующих всех нечетных чисел от 1 < t < N 2 преобразований вида rс (t2 1)(mod N), где
t = х1 хо = N 2хо разность значений, также порождает ещё одну другую тривиальную область значений переменной (rс), обозначаемую (ТССС).

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

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

Устройство моделей натурального ряда чисел (НРЧ) и отдельного составного нечетного натурального числа (СННЧ) можно представить разными математическими зависимостями. Каждая из моделей ее автором ориентируется, приспосабливается к конкретной задаче или группе задач, представляющих интерес для исследования. Здесь будем рассматривать списочную многострочную модель (СММ) составного натурального числа
N = dmdб.

На числовой оси с шагом, равным большему делителю dб, размечаются точки (позиции): dб, 2dб, 3dб, , dmdб аттракторы. После этого аналогичную разметку выполняем для меньшего делителя dm. Последняя точка в обоих случаях совпадает с N.

Между размеченными с разным шагом точками возникает множество интервалов разной длины четной и нечетной, среди которых имеются такие интервалы, которые называются решающими, так как именно они обеспечивают нахождение делителей составного модуля КЧКВ
N = dmdб.

В соответствии с законом распределения делителей (ЗРД) решающий задачу факторизации больших чисел (ЗФБЧ) интервал своими границами [Гл, Гп] должен иметь точки кратные разным делителям N.

Поскольку относительно произвольной точки кратной dб группируются точки кратные dm и их 2 или более слева и справа от dб, то точки кратные dб назовем (Аi) аттракторами (притягивающими). Для решающих интервалов между построенными точками их границы выбираются кратными одна меньшему делителю, другая большему.

Для установления решающих интервалов и их границ выполняем детальный анализ окрестностей точек аттракторов (dб). При разметке числовой оси каждый аттрактор (Аi) как бы накрывается коротким интервалом (величиной из dm позиций, точек), совпадая с одной из промежуточных. Расстояние (в числе точек) аттрактора до ближайших одной из границ накрывающего малого интервала всегда четное, до другой нечетное.

Пример А. Пусть задан составной модуль приведения N = dmdб = 311129 = 34999 конечного числового кольца вычетов. Числовая ось (фрагмент ряда) разбита точками с шагами dm = 31 и dб = 1129. Требуется представить картину подобного разбиения фрагмента на отрезки и определить положение решающих ЗФБЧ интервалов с их характеристиками.

Решение. Начнем с первой (i = 1) точки (аттрактора А1) с координатой равной dб. Эта точка принадлежит замкнутому малому интервалу [1116, 1147 = 1116+31], накрывающему ее, 36dm<1129< 37dm или 1116< 1129 < 1147. Удаленность dб от границ замкнутого интервала [1116, 1147] слева от dб равна 1129 1116 = 13 нечетному числу и справа от dб равна четному числу 1147 1129 = 18.

Правый интервал [Гл, Гп] = [1129, 1147] имеет целочисленную среднюю точку с координатой хц = (Гл + Гп) = (1129 + 1147) = 1138. В этой точке хц ее квадратичный вычет по заданному составному модулю N (в соответствии с ЗРД) должен быть равен полному квадрату, в чем легко убеждаемся, 11382(mod 34999) = 81 = 92 и этот результат указывает на то, что этот интервал решающий, т. е. обеспечивает вычисление делителей модуля N. Вычисления делителей пока отложим.

Перейдем к рассмотрению интервала [1116, 1129] слева от А1 аттрактора, длина которого 13. Он не имеет целочисленной средней точки и не является решающим, но положение легко исправимо, переносом левой границы (Гл) в другую точку более близкую к началу координат (дальше от аттрактора).

Исправленный новый интервал [Гл, Гп] = [1116 31, 1129] решающий и он приобретает вид [1085, 1129] с изменившейся длиной 1129 1085 = 44, т. е. уже имеет среднюю точку, координата которой равна хц = (Гл+ Гп) = (1085 + 1129) = 1107.

В этой точке, следуя ЗРД, квадратичный вычет по заданному составному модулю также должен быть равен полному квадрату. В этом легко убедиться выполнив вычисления 11072 (mod 34999) = 484 = 222 , что также обеспечивает в соответствии с ЗРД вычисление делителей модуля.

Таким образом, найдены два (справа и слева) решающих ЗФБЧ интервала для аттрактора А1, т. е. точки 1dб, ближайшей (i = 1) среди кратных к началу координат. Зададимся вопросом ограничено ли количество решающих интервалов для этого аттрактора? Если да, то чем определяется это ограничение?

Сделаем пояснения. Квадратичные вычеты (за пределами области строк ТКВК), являющиеся полными квадратами это всегда КВК центров решающих интервалов. Они являются дублями КВК из ТКВК. Их количество в ТКВК конечно и допустимое значение наибольшего из них ограничено.

В рассматриваемом примере порог maxКВК = rлmax =N=34999=187,080, т. е. превышает 6-кратную длину малого интервала 631 = 186 < 187, но 7-кратное значение интервала превышает это значение maxКВК 731 = 217 > 187.

Следовательно, все значения КВК (дубли из ТКВК) могут принимать значения, не превышающие 187. Что касается количества решающих интервалов, группируемых аттракторами, то анализ ситуации показывает следующее.

Мощность множества ТКВК это 187 элементов кольца, из которых не дублируются КВК кратных значений kmdm, km = 1(1)(187/ dm). В примере таких шесть значений 31, 62, 93, 124, 157, 186. Строки СМ-модели в своем составе содержат строки, кратные разным делителям.

Строк кратных dб всего 15 = (dm 1), так в каждой из них сумма номера строки хо и его дополнения х1 = N хо остается постоянной равной N, т. е. дополнение также кратно числу dб.

Из анализа нашего примера следует, что существует всего 15 аттракторов Аi, i = 1(1)15 и с каждым из них связаны (187 6)/15 = 12,06 решающих интервалов, с центрами которых связаны дубли из ТКВК. Другими словами, сдвигаясь к началу координат с шагом 31, увеличиваем решающий интервал, КВВ в центре которого получаем новый полный квадрат.

Пример Б. Используя данные примера А, вычислим решающие интервалы, наиболее удаленные влево и вправо от аттрактора А1. Левые интервалы:
[Гл, Гп] = [1116 62, 1129] = [1054, 1129] этот интервал не имеет целочисленной центральной точки хц = (Гл +Гп) = (1054 + 1129) = 1091,5.
Обе границы интервалов должны быть нечетными числами.

Сдвигаемся еще на один dm интервал [1116 93, 1129] = [1023, 1129] и находим центр
хц = (Гл + Гп) = (1023 + 1129) = 1076, в котором
КВВ = КВК = 10762 (mod 34999) = 2809 = 532 .

Заметим, что положение центров (53 22 = 31) интервалов, как и их протяженности (rл) изменяются на величину кратную dm, а левой границы на 2dm. Таким образом, левые (правые) границы решающих интервалов и их центры принимают конкретные значения (здесь граница 1129 постоянная):



Зачеркнутые числа выходят за пределы допустимых элементов КЧКВ
Для аттрактора А2 = 2dб и всех последующих вычисления аналогичные. Их результаты приведены в таблицах 1-15.

В работе здесь рассмотрена контурная модель, связанная с изучением задачи факторизации больших чисел (ЗФБЧ). Особенность модели состоит в том, что в ней НРЧ представлен непрерывной последовательностью нумерованных (номер k = 1(1) ) контуров (блоков), размер которых (L(k)) с увеличением номера возрастает, но остается кратным числу 8, L(k) = 8k.

Каждый контур представляет множество позиций НРЧ, где крайние позиции содержат квадраты нечетных последовательных чисел. Эти крайние позиции называются границами: левая
(Гл(k) = (2k 1)2) и правая (Гп(k) = (2k + 1)2, Гп(k) > Гл(k)).

Все контуры образованы двумя полуконтурами с размерами левый полуконтур m(k) = 4k 1 и правый контур M(k) = 4k+1. Полуконтуры в k-м контуре имеют общую границу
Гц(k) = (2k)2 квадрат четного числа.

Длина (размер) каждого полуконтура нечетное число и M(k) m(k)= 2, а их сумма равна
M(k) + m(k) = L(k) длине k-го контура, левая и правая границы которого совпадают с левой границей меньшего и с правой большего полуконтура.

С введением понятий контура и полуконтура модель НРЧ можно представить как непрерывную последовательность нечетных чисел 1, 3, 5, , в которой каждое число является полуконтуром некоторого k -го контура, где при известном значении N полуконтура, его номер определяется соотношением
k =(N 1).

Знак (+) соответствует левому полуконтуру и () правому. Любое составное нечетное натуральное число (СННЧ) N >9, будем рассматривать как полуконтур или интервал в НРЧ с длиной N. Длина интервала нечетное число равное сумме нечетного количества последовательных слагаемых (полуконтуров), в которой количество слагаемых равно меньшему делителю N, а среднее слагаемое большему делителю N.

В действительности нам известны лишь N и, что оно составное, равно произведению двух простых чисел.
Рассматриваются составные нечетные натуральные числа (СННЧ) среди которых нет чисел N =k2 полных нечетных квадратов.

Дело в том, что такие квадраты дают другую картину распределения квадратичных вычетов (КВВ) элементов кольца, отличающуюся от картины для полупростых чисел N = pq, p < q. Квадратичные вычеты для N = k2 из тривиальной области не дублируются в списочной многострочной модели (СММ) числа и закон распределения делителей (ЗРД) в такой ситуации не работает.

Напомним, что в СММ при дублировании одного из вычетов строки дублируются и другие два. При этом, если делители p и q не очень близки (далеки) друг от друга, то в области тривиальных строк ТКВК имеются две строки, содержащие средние вычеты со свойством, сохранять смежность сомножителей (ССС).

Утверждение. Для двух нечетных произвольных простых чисел p и q их сумма и разность четные и либо их сумма = (р + q), либо их разность = (р q) делится на 4.

Расстояние (в числе строк) между этими строками равно меньшему делителю р. Номер верхней строки с дублируемым средним вычетом определяется соотношением хов = (р q):4. Отсюда номер нижней строки равен хон = хов + р. Положение этих (верхняя и нижняя) строк определяются элементами колонки Тп, которые кратны меньшему делителю.

В случае, когда разность делителей = (р q) не делится на 4, а сумма делится, то номер верхней строки равен хов = хон , где хон = (р + q):4.
В области строк СММ тривиальных средних вычетов, также появляются две строки, содержащие средние вычеты и дублируемые левые КВВ, с теми же значениями вычетов, что и в верхней части модели.

Области аттракции модуля сравнения N = 34999






Размах области аттракции постоянный для всех аттракторов (равен 341 строке), кроме той, что содержит инволюцию (увеличен на dm =31 строку).
Между областями аттракции интервалы либо 766, либо 797 строк. Области хорошо разделены, хотя их влияние друг на друга не исключается.

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

Выводы



Основой списочной многострочной модели (СММ) числа N является модифицированное конечное числовое кольцо вычетов по модулю составного числа N;

Появление квадратичного вычета (полного квадрата) в некоторой точке конечного числового кольца вычетов (КЧКВ) вне пределов тривиальной области квадратичных вычетов полных квадратов (ТКВК) указывает на существование симметричного относительно нее интервала, в котором она является центральной; это решающий интервал;

В соответствии с законом распределения делителей (ЗРД) решающий задачу факторизации больших чисел (ЗФБЧ) интервал своими границами [Гл, Гп] должен иметь (и имеет) точки кратные разным делителям N;

Соотношение ЗРД (di=НОД(N, xоxо2(mod N))) обеспечивает получение (вычисление) делителей N.
Подробнее..

AES АМЕРИКАНСКИЙ СТАНДАРТ ШИФРОВАНИЯ, ЧАСТЬ I

30.06.2020 18:10:31 | Автор: admin
image

Эта публикация вызвана необходимостью дать возможность обучаемым изучать и моделировать процессы шифрования/расшифрования и дешифрования последнего стандарта США. Ознакомление с имеющимися публикациями в сети не соответствуют программе обучения в силу их поверхностного подхода, неполноты изложения, и отсутствия должной строгости. Например, нигде не встречается выбор и задание примитивного элемента, формирующего поле, без чего работу и подготовку специалиста, особенно криптоаналитические явления и процессы, организовать и моделировать невозможно. В этой работе используется описание, несколько отличное от оригинала AES, представленного FIPS PUB 197. Здесь описывается шифр AES, с использованием матриц над GF(28), но примечания работы сохраняются, т. е. шифр реализуется над конечным расширенным полем GF (28). На русском языке достаточно полная и доступная версия шифра изложена Зензиным О.С. и Ивановым М.А.


МАТЕМАТИЧЕСКИЕ ОСНОВ СТАНДАРТА ШИФРОВАНИЯ AES США


AES блочный шифр с длиной блоков равной 128 битам, и шифр поддерживает ключи длиной $N_к$, равной 128, 192 или 256 бит. AES это шифр с итерационным ключом: состоит из повторного применения раундового преобразования состояния блока State шифруемого текста. Число раундов шифра обозначается $N_r$ зависит от длины ключа ($N_r = 10$ для ключа 128 битов, $N_r= 12$ для ключа 192 бита и $N_r = 14$ для ключа 256 бит).

Шифр AES преобразует исходное состояние блока, обозначаемое символом S (State) и принадлежащее множеству матриц {M4(GF(28))} (то есть S {M4(GF (28))} матрица 4 4 байта, с ее элементами (коэффициентами) в GF (28)), к другому шифрованному состоянию в {M4 (GF (28))}.

Пример 1. Блок данных длиной в 128 = 432, 4 слова по 32 разряда представляется квадратной таблицей байтов из 4-х строк и 4-х столбцов. Каждая строка содержит байты из 4-х разных 32 разрядных слов, а столбец байты одного и того же 32-разрядного слова. Весь квадрат образован 44 = 16 байтами, которые могут обрабатываться как самостоятельные единицы.

Именно такой подход к представлению данных определяет и обеспечивает байт-ориентированную структуру шифра и обработку данных. Ключ шифра K расширяется в $N_r+1$ подключей, обозначаемых матрицей Ki ={M4(GF(28))}, принадлежащей множеству {M4 (GF(28))}, (i = 0, 1, ..., Nr). Перестановка элементов в матрице S, возникающая при операции сдвига строк обозначена символом р(х).

Представление данных, выбранное для элементов поля GF(28)


В шифре AES используется байтовая структура данных. Представление, выбранное в [1] из векторного пространства GF(28), соответствующего полю GF(28)[X]/< (x) >, где (x) неприводимый многочлен,

$(x) = x^8 + x^4 + x^3 + x + 1$

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

Таблица 1. Соответствие десятичных, шестнадцатеричных, двоичных чисел и многочленов

В работе используются четыре представления для обозначения каждого элемента расширенного поля в GF(28), которые являются эквивалентными одно другому.

Представление данных, используемых в шифре AES


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

1. 21210, десятичным видом числом в 10-ой системе счисления.

2. {11010100}b, представление элементов сообщения двоичным вектором элементом векторного пространства GF(28) двоичных векторов,

3. $x^7+ x^6 + x^4 + x^2$, многочленное представление элементом поля Галуа GF [28], соответствующим двоичному вектору,

4. {D4}16, шестнадцатеричное представление числом в 16-системе счисления,

операция поразрядного суммирования векторов из GF(28) по mod2 (без переноса 1 в старший разряд).
операция умножения элементов (векторов, многочленов, чисел) из поля GF(28)

Алгоритм стандарта AES и шифра RIGNDAEL оперирует с байтами информации, которые отождествляются с элементами конечного поля Галуа GF(28). Степень расширения простого поля GF(2) равна 8. Все элементы расширенного поля при представлении их многочленами имеют показатель степень не более семи ( 7).

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

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

$(x) = x^8 + x^4 + x^3 + x + 1$

или 1{1b} в 16-ричной форме.
Шестнадцатеричная запись неприводимого многочлена 1{1b} использует 9 разрядов и многочлен (x) не принадлежит полю GF(28).
Таблица П1 представления элементов поля GF(28) (в конце текста в Приложении).
В таблице П1 размещены все элементы поля в порядке возрастания показателя степени примитивного элемента ( = 000000112 = 310), мультипликативный порядок которого равен 255.

Рассмотрим примеры выполнения арифметических операций над элементами поля при различных представлениях этих элементов. Любой байт исходного текста (элемент поля) формально можно представить строкой символов $a_i, i = 0(1)7$, коэффициентов двоичного вектора в виде:

${a_7, a_6, a_5, a_4, a_3, a_2, a_1, a_0}, a_iGF(2), i = 0(1)7.$


Пример 2. Элемент расширенного поля GF(28) задан в виде двоичного вектора:

${a_7, a_6, a_5, a_4, a_3, a_2, a_1, a_0}, a_iGF(2), i = 0(1)7$


Описание многочленом этого элемента имеет вид:

$(x) = a_7 x^7 + a_6 x^6 + a_5 x^5 + a_4x^4 + a_3 x^3 + a_2 x^2 + a_1 x^1 + a_0 x^0$


Если доопределить значения ai двоичными значениями, i = 0(1)7, например, так ${a_7, a_6, a_5, a_4, a_3, a_2, a_1, a_0}$ = {11000001}2, то получим многочлен

$(x) = x^7 + x^6 +1,$

так как $ a_5 = a_4 =a_3 =a_2 =a_1 = 0.$

Шестнадцатеричное представление этого элемента 16 = {с1}={11000001}, а десятичное
10 = $2^7 + 2^6 + 2^0$ = 128 + 64 + 1 = 19310.

При выбранном примитивном элементе поля степенное представление
i ={с1}= 178.
Входим в таблицу П1 элементов поля GF(28) со значением 178 и в соответствующих столбцах для этой строки находим описанные представления, а также другие характеристики этого элемента поля. Для понимания возможных преобразований с элементами поля рассмотрим операции с его элементами в деталях.

Суммирование элементов поля GF(28)


Сложение в рассматриваемом поле представляет операцию поразрядного суммирования значений разрядов слагаемых без переноса единицы в старший разряд. Это операция исключающего ИЛИ (EXOR EXLUSIV OR) часто обозначается просто XOR. В модулярной арифметике такое сложение называется суммированием по модулю два (mod2).

Пример 3. Выберем в качестве операндов многочлены
$A(x) = x^7+ x^6 + 1;$
$B(x) = x^3 + x^2 + x^0.$
Двоичное представление суммы многочленов по модулю два имеет вид
[A(x)B(x)]mod2 = {11000001} {00001101} = {11001100};

16-ричное представление {c1}{0D}={cc}sub>16=55;
степенное представление 178 + 238 = 55 {123 + 183} = 55 1 = 55.

Представление многочленами
$(x^7+x^6+1) (x^3+x^2+1)(mod2)= (x^7+x^6+x^3+x^2+2)(mod2)=x^7+x^6+x^3+x^2.$
Заметим, что при сложении операндов степень многочлена результата не
увеличивается, и необходимость приведения его по модулю неприводимого многочлена поля (x) не возникает. Коэффициенты результата приводятся по модулю два, т. е. все четные коэффициенты обращаются в нуль.

В полях характеристики 2 действия сложения и вычитания операндов равнозначны. Для каждого элемента поля в аддитивной группе обратным к нему (противоположным) является он сам. Так, для элемента (а) противоположным является (-а), так как а + (-а) = 0. Нулевой элемент (единица аддитивной группы поля, нейтральный элемент) в шестнадцатеричном виде это {00}16.

Умножение элементов поля GF(28)



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

Пример 4. Умножение операндов в двоичном представлении
А(х) В(х) = {c1}{0d}

Остаток от деления получает вид двоичного, многочленного и 16-ричного представлений (как элемент поля)
R(x) = 01011 10102 = $x^7+ x^5+ x^4+ x^3+ x $= {ba}16. Старший разряд остатка равен нулю и не учитывается.
Степенное представление здесь не приводим, но по таблице П1 его можно найти.

Остаток от деления на неприводимый многочлен (x) в его двоичном представлении результата умножения операндов принимаем в качестве произведения операндов как элементов поля GF(28).
Выполним умножение операндов в представлении многочленами.

Пример 5. Умножение операндов элементов поля в многочленном представлении
А(х) В(х) = {c1}{0d}
A(x) B(x) = $(x^7+ x^6+ 1)(x^3+ x^2+1) $(modd(x),2) =
=(x10+$ x^9+ x^9+ x^8+ x^7+ x^6+ x^3+ x^2+1)$(modd(x),2) =
=(x10+$ x^8+ x^7+ x^6+ x^3+ x^2+1)$(modd(x),2).

Здесь символ (modd (x),2) обозначает приведение по двойному модулю: многочлен по модулю (x), а его коэффициенты по модулю два, т.е. четные коэффициенты обнуляются. Получившаяся степень (deg(A(x) B(x)) =10) результата произведения выводит (этот многочлен результат) за пределы поля. Чтобы результат принадлежал полю, его приводят (редуцируют, делят) по модулю неприводимого многочлена. Выполним такое деление обычным способом (уголком)

Пример 6. Необходимость деления многочленов возникает при
их умножении А(х)В(х)/ (x).(Операции деления в поле нет, когда надо что-то поделить, это что-то умножают на обратный элемент делителя в поле)

остаток отделения на (x) принадлежит полю GF(28), и его принимаем в качестве окончательного результата $R(x) = x^7+x^5+x^4+x^3+x$ модулярного умножения.

Иначе умножение A(x)B(x) представимо как
$(x^2 + 1)(x^8 + x^4+ x^3+ x +1) (x^7+ x^5+ x^4+ x^3+ x) = = (x^2+1)(x)R(x)$ = {ba}16=161,
где R(x) остаток и degR(x)< deg (x).
Степенное представление для получения произведения элементов поля самое удобное.
A(x) B(x) = 178 238 = (178+238) = 416 = 161255 =161 = {ba}16.

Для любого ненулевого элемента поля справедливо 1 =. Мультипликативной единицей в GF(28) является элемент {01}16 =255.
Все вычисленные произведения для различных представлений операндов эквивалентны (преобразуются в один элемент поля со значением {ba}16).

Наряду с обычным (классическим) рассмотрением операции умножения элементов в двоичном поле существует более удобная схема. Именно такая схема и реализована в стандарте AES.

Рассмотрим сущность этого способа умножения



Пример 7. Другой способ умножения в конечном поле. Пусть задан произвольный многочлен седьмой степени
$A(x) = a_7x^7+ a_6x^6+ a_5x^5+ a_4x^4+ a_3x^3+ a_2x^2+ a_1x+ a_0$
и значения его коэффициентов $(a_7 a_6 a_5 a_4 a_3 a_2 a_1 a_0) = (10000011)_2$.

Умножим его на $x$ и получим $a_7x^8+ a_1x^2+ a_0x$. Этот результат не принадлежит полю GF(28) степень его многочлена больше 7 и его необходимо привести по модулю (x) = 1{1b}, после чего такое произведение станет элементом поля GF(28).

С этой целью определяют значение $ a_7 $, если $а_7 = 0$ то результат уже принадлежит полю, если же $ a_7 = 1$, то достаточно вместо деления выполнить лишь вычитание A(x)x (x) или операцию XOR для произведения A(x)x с (x).

В этом случае при записи A(x) в сдвиговом регистре умножению на x полинома A(x), т.е. $A(x){00000010} = A(x){02}$ соответствует сдвиг полинома A(x) на один разряд в сторону старших разрядов (влево, т.е. увеличение вдвое) и, если требуется, применяется операция XOR с неприводимым многочленом поля 1{1b}16 =(x).

В алгоритме стандарта шифрования введена операция (функция) xtime( ), которая по существу и выполняет умножение полинома на х, так как это описано ранее. Многократное применение xtime обеспечивает умножение на x8. Далее вычисляя сумму различных степеней, можно получить результат умножения произвольных элементов поля GF(28).

Пример 8. Перемножим многочлены A(x) = {c1}16 и B(x) = {11}16, используя их 16-ичные представления и представляя суммой {11} ={1001}.
{c1}{11}={11000001}{00010001}={c1}{1001}={a4}{c1}=01100101 ={65}16.

Детализируем все действия. Элемент х в поле GF(28) имеет представление
x = {02}16=(00000010)2.
{c1}{11}={c1}{10}{c1}{01}= 178 100178 0={a4}{c1}, так как 178 100=(178+100-255)=23={a4}

Тогда умножение на него приводит просто к сдвигу первого операнда на 1 разряд влево.
{c1} {02} = xtime{c1} = 11000001 00000010= 110000010 9-ти разрядное двоичное число. Этот результат выходит за пределы нашего поля. Его возвращают вычитанием неприводимого многочлена поля (х), преобразуя в элемент поля. Итоговый результат 10011001 = {99}

$\begin{array}{r} - \begin{array}{r} 110000010\\ 100011011\\ \end{array} \\ \hline \begin{array}{r} 10011001 \end{array} \end{array}$


{c1} {04} = xtime(99) = 10011001 00000010 =100110010 9-ти разрядное двоичное число. Этот результат выходит за пределы нашего поля. Его возвращают вычитанием неприводимого многочлена поля (х), преобразуя в элемент поля. Итоговый результат 00101001 = {29}

$\begin{array}{r} - \begin{array}{r} 100110010\\ 100011011\\ \end{array} \\ \hline \begin{array}{r} 00101001 \end{array} \end{array}$


Очередной шаг процедуры
{c1} {08} = xtime(29) = 00101001 00000010 = 0101 0010 = {52}.
Здесь результат не суммируем с (x), так как коэффициент $a_7 = 0$.

И еще один шаг
{c1} {10} = xtime(52) = 0101001000000010 = 10100100 = {a4}16.
Здесь также не суммируем с (x), так как коэффициент $a_7 = 0$.
Таким образом, найдено значение первого слагаемого в сумме для исходного
выражения, где второе слагаемое равно {c1}16.
Теперь находим окончательно
{c1} {11} = {c1} {10} {c1} {01} = {a4} {c1} =10100100 11000001 = {65} или

$\begin{array}{r} - \begin{array}{r} 10100100\\ 11000001\\ \end{array} \\ \hline \begin{array}{r} 01100101 \end{array} \end{array}$



проверка обычным умножением (степенное представление)
A(x) B(x) = {c1}{11} = 1784 = 182
(по таблицам находим в строке для 1182) 182 соответствует {65}16.

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

Операция умножения таких слов
$A(x) = a_3x^3 + a_2x^2 + a_1x + a_0$ и
$B(x) = b_3x^3 + b_2x^2 + b_1x+b_0$,
где $a_i, b_i$GF(28), i = 0(1)3, выполняется по модулю многочлена степени не более четырех. Взятие результата произведения по модулю неприводимого многочлена степени 4 обеспечивает всегда получение результата произведения, как элемента поля.

В качестве такого многочлена выбран многочлен $(x) = x^4 + 1$. Он имеет наиболее простую запись, и для него справедливо
$x_i (mod(x^4+1)) = x_i (mod4)$.
Последнее свойство оказывается очень полезным при вычислениях.
Для рассматриваемых многочленов операция сложения выполняется аналогично (XOR поразрядное по mod2)
$inline$A(x) B(x) = (a_3 b_3) x^3 (a_2 b_2) x^2 (a_1 b_1)x (a_0 b_0)$inline$.

Умножение многочленов.
$inline$A(x) B(x) = C(x) = (c_6x^6 c_5x^5 c_4x^4 c_3x^3 c_2x^2 c_1x^1 c_0) mod(x^4+1)$inline$.
Коэффициенты $c_j, j = 0(1)6$ определяются из соотношений
$C_0 = a_0b_0$,
$C_1 = a_1b_0 a_0b_1$,
$C_2 = a_2b_0 a_1b_1a_0b_2$,
$C_3 = a_3b_0 a_2b_1 a_1b_2a_0b_3$,
$C_4 = a_3b_1 a_2b_2 a_1b_3$,
$C_5 = a_3b_2 a_2b_3$,
$C_6 = a_3b_3$.

Окончательно результатом D(x) умножения двух многочленов по модулю $x^4+1$ будет
$inline$D(x) = A(x)B(x) = d_3x^3 d_2x^2 d_1x d_0$inline$, где
$d_0 = a_0b_0 a_3b_1 a_2b_2 a_1b_3$,
$d_1 = a_1b_0 a_0b_1 a_3b_2 a_2b_3$,
$d_2 = a_2b_0 a_1b_1 a_0b_2 a_3b_3$,
$d_3 = a_3b_0 a_2b_1 a_1b_2 a_0b_3$,
или более кратко в векторно матричной записи,

выполним умножение В(х) на х по $mod(х^4+1)$, учитывая свойства многочлена. Такому умножению, как и ранее, соответствует циклический сдвиг байтов в пределах слова в направлении старшего байта. Так как
$x^4mod(x^4 + 1) = x_i mod4 = x^0 = 1$, то
$x B(x) = b_2x^3+ b_1x^2+ b_0x+ b_3 => x(b_3 b_2 b_1 b_0)$
реализуется циклический сдвиг байтов.

Приложение


Таблица П1 расширенного поля, неприводимый многочлен (х)=Р (х), примитивный элемент =316



Подробнее..

АES американский стандарт шифрования. Часть II

03.07.2020 16:05:03 | Автор: admin
image

Основные операции шифра


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

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

Продолжая тему о блочном шифре победителе конкурса, проводимого Национальным институтом стандартов и технологий США (1997 2000), будем рассматривать его функционирование, работу. Агентство национальной безопасности США не являлось участником конкурса, поэтому шансы стать победителем у разработчиков были велики.

Конкурсной комиссией были предварительно отобраны 15 заявок от разных стран. Выступать могли как организации, так и частные лица. В итоге победил RIJNDAEL(Бельгия) авторы Винсент Рюмен (Vincent Rijmen) и Ион Дэмен (Joan Daemen). Структуру шифра считают классической SP-сетью.

Ранее уже приводились основные технические характеристики и повторяться не будем.

Шифрование сообщений


Разработчиками создан шедевр, впервые идеология и реализация симметричного блочного шифра стала до конца понятной, так как шифр полностью основан на элементах классической (прозрачной) алгебраической структуры (поле), с достаточно скромными для обозрения количественными характеристиками. Конечное расширенное поле (поле многочленов) характеристики 2 имеет степень расширения n=8 и число элементов (порядок поля 28 = 256), отсюда обозначение поля GF(28).

Выбор неприводимого многочлена поля $(x) =x^8 +x^4 + x^3 + x + 1$ и примитивного элемента поля = 000000112 = 310, также не является загадочным. Одним словом, это не DES и не отечественный ГОСТ 28147.89.
Исходные тексты подготавливаются на устройствах с клавиатурой, используя
Таблицу Символы кода ASCII



Раунды и операции шифрования АЕS


Число раундов шифра обозначается Nr и зависит от длины ключа (Nr = 10 для ключа 128 битов, Nr= 12 для ключа 192 бита и Nr = 14 для ключа 256 битов).
Цикл (один раунд) шифрования AES состоит (в изменении состояния State шифруемого сообщения) из четырех основных операций:
1. AddRoundKey S + Ki; суммирование состояния (исходного текста с ключом)
2. SubBytes ax-1 + b; замена результатов суммирования другими байтами
3. ShiftRows; Sp(x); циклические сдвиги строк на разное число позиций
4. MixColumns A0S. перемешивание столбцов квадрата.

Покажем, как это происходит на числовом примере для текущего состояния в каждой операции, начиная с верхней (нулевой) строки. Результаты 2-й и 3-й операций не требуют числовых преобразований, но MixColumns связана с умножением 2-х матриц и определение элементов результирующей матрицы требует конкретных вычислений в поле GF(28). Алгоритм выработки ключа (Kеу Schedule) формирует 32-разрядные слова, число которых равно длине блока Nb.

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

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

Операция AddRoundKey для i-го раунда


Суммирование столбцов State с раундовым ключом (Add Round Key).

Раундовый ключ, предварительно вычисленный и выбранный, представляется в форме (квадрат) State (Key). Суммирование выполняется для одноименных столбцов. Пара столбцов State (Data) и State (Key) операцией XOR поразрядно складывается.

Таким образом, преобразование AddRoundKey состоит из суммирования матрицы S блока (квадрата 44) сообщения в M4(GF(28)) с частичным Ki ключом i-го раунда (также с квадратом 44 ). Результат суммирования обозначается SiA, т е. состояние после i-го AddRoundKey M4(GF (28)) M4(GF (28)), S SiA = S + Ki.
В начальном раунде (r = 0) выполняется только Add RoundKey суммирование по mod2 (XOR) байтов блока данных и блока ключа. Результат этого раунда используется как исходное (State) состояние для первого раунда (r=1) и представляется в формате квадрат.
Шифрование сообщений AES

Пример 1. Сообщение (исходный текст = ИТ) в 16-ричном представлении
имеет вид 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34,
а ключ шифра 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 0f 4f 3c.
Операция AddRoundKey S + Ki для всех 16 байтов квадрата State
Предварительный раунд зашифрования подготавливает сумму ИТ с ключом.

Их Сумма, представленная байтами в формате квадрат при Nb = Nk = 4 и Nr = 10 имеет вид:


Рисунок Результат предварительного раунда (до первого)

Ключ и исходный текст (это может быть и звук и картинка и др.) представлены в 16-ричном виде. Это не самое удобное представление, поэтому преобразуем его к двоичным векторам и выполняем сложение по модулю 2.



Для удобства ручного счета 16-ричное представление байтов преобразуется в двоичное, а после суммирования обратный переход от двоичного к 16-ричному представлению.

Операция SubByte для i-го раунда


Замена байтов (Sub Bytes ( ) )

Первый раунд. Операция AddRoundKey уже выполнена. Все байты суммарного квадрата (исходного состояния обозначим их точками (х, y) на некоторой плоскости) преобразуются функцией S (x, y) в новые значения табличка "Замена байтов".

Заменяем угловой (левый верхний) байт State = Сумма, (равный 19), на байт d4 из таблицы замен S-блока и следующий (3D) на 27 (они выделены заливкой). Так преобразуются все 16 байтов квадрата "Сумма".

Так для первого байта {19} = {x, y}, x = 1, y = 9, S(19) = d4. Все такие значения также представляются квадратом с замещениями.

Функция S (x, y) табулирована, т. е. задана таблицей с двумя входами строка (х) и столбец (у). Поскольку все многообразие байтов исчерпывается числом 256, то таблица имеет размер 1616 = 256, а ее строки и столбцы пронумерованы x, y = 0(1)15 цифрами 16-ричной системы счисления. Вид таблицы S (x, y) приведен ниже.



Покажем, как получены значения этой таблицы S (x, y). Вначале каждый байт исходного состояния {x, y} преобразуется в инвертированный {x, y}-1, в мультипликативный обратный в поле GF(28). Затем байт умножается на матрицу А. Нулевой байт переходит в себя {0, 0}-1 = {0, 0}.

Каждая строка матрицы аффинного преобразования А содержит лишь пять единиц.

Затем к инвертированному (обращенному) байту применяют аффинное преобразование, которое в векторно-матричной форме записывается так



Аффинная матрица

Запись векторов сверху (младший разряд) вниз. Вектор сдвига c = {63}16, что в таблице S (x, y) отражено для байта {00}. Каждый байт исходного состояния подвергается такому преобразованию, и их значения вписаны в квадратную таблицу S (x, y).

Скалярное представление аффинного преобразования имеет следующий вид (общий вид i -го элемента результата):


где $c_0 = c_1 = c_5 = c_6 = 1; c_2 = c_3 = c_4 = c_7 = 0$; $b_i$, $b_i$' соответственно исходное и преобразованное значение i-го разряда байтов, i = 0(1)7.

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

Для ускорения вычислений используется квадратная таблица фиксированных значений (S (x, y) блок). Значение b =63 16 ричное число принадлежит GF(28) и матрица а имеет вид, показанный ранее. В сущности, операция реализует подстановку (замену) байт из S квадрата 1616.

Преобразование SubByte состоит в применении к каждому элементу матрицы S элементарного преобразования s. Результат этой операции обозначается SiSu, т. е. это состояние блока текста после операции SubByte i-го раунда. Не путать буквы S и s (большую и малую).

В сущности, здесь выполняется аффинное преобразование с постоянным вектором сдвига, обозначаемым символом b
M4 (GF(28)) M4 (GF(28)).

Запись векторов сверху (младший разряд) вниз. Вектор сдвига c = {63}16, что в таблице S (x, y) отражено для байта {00}. Каждый байт исходного текста (состояния) подвергается такому преобразованию, и их значения вписаны в таблицу S (x, y).

Пример 2. Первый байт исходного состояния первого раунда равен {19}. Мультипликативный обратный для него (см. Табл. П1) равен 3f = {19}-1= 0011 11112 =113. Это значение легко находится по таблице элементов поля GF(28). При отсутствии таблицы элементов поля можно выполнить и непосредственные вычисления.

Пусть байт {x, y} = 09 = 199 и {09} -1 = 4f, тогда
b0 = (1+0+1)mod2 = 0, b4 = (1+1+0)mod2 = 0,
b1 = (1+0+1)mod2 = 0, b5 = (0+1+1)mod2 = 0,
b2 = (1+0+0)mod2 = 1, b6 = (0+1+1)mod2 = 0,
b3 = (1+1+0)mod2 = 0, b7 = (0+1+0)mod2 = 1.
Результирующий вектор ( 0, 1, 2, 3, 4, 5, 6, 7) =1000 01002 = 84.

Следовательно, байт 4f S-блоком преобразуется в байт 84, что можно увидеть в таблице замен S (x, y) = S (4, f) = 84. Этот результата лежит на пересечении строки с номером 4 и столбца с номером f. Геометрическое представление замены байтов (SubBytes) изображено на рисунке 4, где s нелинейное преобразование, определяемое соотношениями:
GF(28) GF(28),


Операция ShiftRows для i-го раунда


В каждом раунде после замены всех байтов State получен квадрат (средний) исходного состояния для преобразования сдвига строк. Такой сдвиг способствует еще большему перемешиванию данных.

Выбор величин сдвигов подчинен следующим условиям:
Каждая строка должна иметь индивидуальное смещение элементов;
Реализация операции должна быть простой;

Совокупность сдвигов наилучшим образом должна препятствовать проведению атак:

А) атак, использующих сокращенные дифференциалы
В) атак типа Square.


Рисунок 5 Состояние после сдвига строк

ShiftRows преобразование перемещения байтов строки в матрице состояния S, которое циклически сдвигает строки матрицы состояния на различные по величине смещения. Результат операции обозначается SiSh, т. е. это состояние после операции ShiftRows
i-го раунда. Такие перемещения могут быть описаны Р(х) перестановкой байт из квадрата S, которая имеет вид:


Из всех вариантов сдвигов, обеспечивающих наилучшее противостояние атакам, был выбран самый простой циклический сдвиг и c0 = 0.

Геометрическое представление преобразования замены байтов изображено на рисунке 4, где приведены квадраты числового примера для первого раунда.

Сдвиг строк в массиве State выполняется влево. Строка с номером нуль остается неподвижной (не сдвигается). Остальные строки RIJNDAEL сдвигаются на число байтов c1, c2, c3, указанное в квадрате на рисунке 5), в зависимости от числа Nb байтов в блоке данных. Строка 1 сдвигается на c1 байтов, строка 2 на c2 байтов и строка 3 на с3 байтов.

В стандарте AES величина Nb = 128 постоянная, поэтому всегда c1 = 1, c2 = 2 и c3 = 3.

Таблица Величины сдвигов



Операция MixColumns для i-го раунда


Это преобразование применяется для обеспечивания рассеивания символов исходного сообщения, удовлетворения второго после перемешивания требования к шифрующим преобразованиям. Требования к такому преобразованию, предъявляемые теорией синтеза шифров, следующие.

  • Обратимость получаемого результата.
  • Линейность в поле GF(28); выполнение этого требования создает условия для применения алгоритма прямого расшифрования.
  • Достаточно сильное рассеивание данных.
  • Высокую скорость реализации на 8-разрядных процессорах; единообразие обработки любых блоков данных, т. е. симметричность.
  • Простой вид для описания и реализации.

Все названные требования удовлетворены в стандарте АES. Из возможных линейных преобразований 4 bytes 4 bytes выбрана операция умножения многочленов (элементов поля) по модулю (x) = x4 + 1. Выбор коэффициентов сомножителей обусловлен требованиями 1, 3 и 4. Коэффициенты {00}, {01}, {02}, {03}, соответствуют: {00} отсутствию преобразования; {01} не требуется умножения; {02} умножение при использовании функции x time ( ) и {03} при использовании x time и последующего сложения по mod2 промежуточных результатов.

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

Сущность преобразования Mix Columns ( ) состоит в следующем. Столбцы State
S = < S0c, S1c, S2c, S3c>, с = 0(1)3, после сдвига строк рассматриваются как элементы поля GF(28) и умножаются по mod(x4 + 1) на фиксированный многочлен g(x):
g(x) = {03}x3+{0,1}x2+{01}x+{02}.


где с номер столбца State, c = 0(1)3.

где А циркулянтная матрица, т. е. линейное инвертируемое (обратимое) отображение на GF (28), А принадлежит M8(GF (28)),
обозначает умножение матрицы из GF (28) и вектора
х -1 = {b0 b1b7 }b, который рассматривается как элемент над полем GF(2) векторного пространства, эквивалентный транспонированному в вектор {b0 b1b7 }b.

Развернем это выражение в скалярную форму
S0C = ({02} S0C) ({03} S1C) S2C S3C;
S1C = S0C ({02} S1C) ({03} S2C) S3C;
S2C = S0C S1C ({02}S2C) ({03} S3C);
S3C = ({03} S0C) S1C S2C ({02} S3C).
В результате выполнения указанных действий байты вектора
SC = <S0c, S1c, S2c, S3c> будут заменены байтами S0C, S1C, S2C, S3C.

MixColumns преобразование состоит из умножения двух матриц. Матрицы S промежуточного состояния текста и фиксированной матрицы A0 обе из введенного ранее множества M4 (GF (28)). Результат операции обозначается символом SiM, это по существу состояние S после операции MixColumns i-го раунда.

M4 (GF (28)) M4 (GF (28)),
S Si, М = АS,
где матрицы А и А-1 (циркулянтные матрицы) определяются как:

.
Операция MixColumns произведение специальной цикловой А0 матрицы на матрицу State, по правилу строка на столбец

.
Два пустых столбца в результирующей матрице оставлены для самостоятельного заполнения.
Начинаем с левого углового элемента нулевой строки квадрата. Его значение определяется суммой произведений пар элементов: {02}D4{03}BF {01}5D {01}30. Элементы обеих матриц это элементы поля GF(28) с одной стороны и информационные байты в 16-ричном представлении с другой. Умножение элементов в поле удобно выполнять в степенном представлении примитивного элемента.

Пользуясь таблицей поля, выполним замену 16-ричного представления на степенное для левой цикловой матрицы элементы {01} = a0, {02} = a25, {03} = a1. Для правой матрицы в произведении получим


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

1-й элемент нулевой строки определяется в виде:

{02}D4 {03}BF {01}5D {01}30 = a25а65 a1а157 a0а136 a0а101 = а90 a158а136а101,

2-й элемент нулевой строки определяется аналогичным соотношением (поменялся столбец)

{02}Е0{03}B4 {01}52 {01}АЕ = a25а68 a1а251 a0а253 a0а123 = а93 a252 а253а123,

3-й элемент нулевой строки определяется аналогичным соотношением поменялся столбец

{02}B8{03}41 {01}11 {01}F1 = a25а59 a1а143 a0а4 a0а74 = а84 a144 а4 а74,

4-й элемент нулевой строки определяется аналогичным соотношением поменялся столбец

{02}1Е{03}27 {01}98 {01}Е5 = a25а28 a1а106 a0а89 a0а32 = а53a107 а89а32.

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



Перейдем к вычислению элементов 1-й строки матрицы (изменяется номер строки циклической матрицы) Элементы в парах произведений изменяются относительно 0-й строки.

1-й элемент 1-й строки матрицы перемешанных столбцов определяется соотношением.


Перейдем к вычислению элементов 2-й строки матрицы (изменяется номер строки циклической матрицы). Элементы в парах произведений изменяются относительно 1-й строки.

1-й элемент 2-й строки матрицы перемешанных столбцов определяется соотношением



Перейдем к вычислению элементов 3-й строки матрицы (изменяется номер строки циклической матрицы) Элементы в парах произведений изменяются относительно 2-й строки.

Подробнее..

Что такое алгоритм?_? Часть 3.1 Эволюция памяти

05.07.2020 08:18:54 | Автор: admin

Идём в глубь острова сокровищ с названием "Алгоритм".


Title


Задача


Перед Вами новая статья серии "Что такое алгоритм?". И снова непростая задача. Нам необходимо "нырнуть глубже" в структуры памяти живого организма. К сожалению, в ракурсе, предлагаемом статьёй, это направление еще исследовано слабо. Что делает материал, приведённый далее, более сложным по сравнению с излагаемым в предыдущих частях, в которых использовалась близость с методами "генетических алгоритмов", автоматизированных систем управления, систем машинного обучения с учителем и подкреплением. Поэтому прокладывать тропинку в глубь неизведанных "территорий" процессов памяти придется во многом самостоятельно. С одной стороны это очень интересно. Ведь мы с Вами первопроходцы! А с другой стороны есть опасность "заблудиться". . Чтобы приободрить в начале дороги, могу лишь сказать, что путь один раз уже пройден, туры сложены вдоль нашей дороги, карта местности прорисовывается, а клад в конце пути обещает быть внушительным.


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


Для вскрытия структуры памяти потребуются все результаты, полученные в предыдущих статьях серии (Часть 1 "Действие", Часть 2 "Исполнение", Часть 3 "Память"). Без перечисленных там выкладок читать дальше будет сложнее.


Давайте приступим.


Эволюция цепочек


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


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

Если посмотреть на множество алгоритмов в мозге "глазами программиста", то можно увидеть, что их совокупность похожа на программный проект в процессе развития. А что помогает программисту разобраться с незнакомым и запутанным проектом без документации? При наличии в проекте системы контроля версий ему будет полезно использовать анализ истории.


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


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


Первым важным фактором ("двигателем" эволюции) является неравномерность распределения ресурсов и условий в среде обитания. Например, если в месте расположения организма в любой момент времени есть "Еда", то организму никогда не понадобится алгоритм "Двигаться в поиске еды", а вместе с ним может не понадобиться и обязательное действие этого алгоритма действие("Двигаться").


Вторым фактором развития биологических организмов является смерть. Никогда бы не подумал, что придётся сказать фразу следующего содержания, но: "Смерть полезна". Она позволяет выполнить развитие алгоритмов в группе родственных организмов.



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


Этап "Зарождение действия"


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


Как организм может повлиять на действие? Он может его использовать или не использовать. Он может его использовать изредка и часто. Он может исполнять его периодически.


Для организма в ситуации с едой, целесообразно использовать действие("Двигаться") время от времени:


  1. передвинулся в новое место;
  2. остановил движение;
  3. некоторое время не используй движение (вместо него попробуй поесть в новом месте);
  4. продолжи с пункта 1.

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


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


Первый алгоритм собран! Он тривиален, но уже может работать (помогать организму выжить). Его реализация доступна даже на биологической элементной "функциональной базе", а у программиста эта простая задача даже интереса не вызовет. Но перед тем как усложнить программисту ТЗ, давайте введём структуру описания "коммита" (этапа эволюции памяти).


Для каждого этапа развития памяти необходимо указать следующие сведения:


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

Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Двигаться полезнее чем находиться в одном месте Время от времени организм выполняет действие и это ему полезнее чем не выполнять "Накопление потенциала разрядка исполнением" Бесконечный цикл для исполнения операции с настраиваемым интервалом

stage action


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


Этап "Формирование торможения"


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


Значит накапливаемый потенциал исполнения действия должен быть внутренним процессом, и как показывает рассмотренный пример "убегания от еды", даже при накопленном потенциале полезно не использовать действия("Двигаться"), если в текущем месте много еды. Мы нашли новый эволюционный фактор! Значит надо делать шаг развития.


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

stage suppression


Этап "Формирование рефлекса"


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


Пример эволюционного признака Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Полезнее двигаться-убегать при наличии опасности чем оставаться на одном месте Организм обязательно начинает выполнение действия при наличии признака "Возбуждение", то есть старт разрядки по признаку Бесконечный цикл со спящим ожиданием установки флага для исполнения операции

stage excitation


Этап "Универсализация"


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


Первой рассмотрим универсализацию нескольких ограничений.


Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Вредно убегать при наличии в текущем месте запаса еды или особи для спаривания Время от времени организм может выполнять действие, но не начинает его выполнение при наличии ограничивающего признака("1") или при наличии ограничивающего признака("2") - Бесконечный цикл со спящим ожиданием (снятия флага1) OR (снятия флага2) для исполнения операции

stage suppression2


Универсализация нескольких признаков для рефлекса представлена далее.


Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Убегать от хищника и убегать от огня полезнее чем оставаться на одном месте Время от времени организм может выполнять действие, но обязательно его выполняет при наличии признака("1") или при наличии признака("2") - Бесконечный цикл со спящим ожиданием (установки флага1) OR (установки флага2) для исполнения операции

stage excitation2


На этом этапе простая и классическая часть повествования закончилась. И мы переходим к интересным "сложностям".


Этап "Соревнование стратегий"


Как и прежде этап начинается с неприятного подарка среды, подготовленного для "многострадального" организма. Этот "подарок" возможность нахождения и "еды", и "опасности" в одном месте. Организму никак не выработать алгоритм поведения на основе типов алгоритмов, которые имеются в его арсенале. Значит "придётся" развиваться.


Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Убегать от хищника полезно даже если рядом много еды Время от времени организм может выполнять действие, но обязательно его выполняет при наличии приоритетного признака("1") даже при наличии ограничивающего признака("2") Приоритет признаков Бесконечный цикл со спящим ожиданием положительного соотношения приоритетов флага1 и флага2 для исполнения операции

stage competition


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


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


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


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


Этап "Группировка признаков"


Цепочки памяти во всех предыдущих этапах были односвязными. То есть в них запоминалась (то есть укреплялась) и взвешивалась (то есть оценивалась пользой) одна связь "признак-действие". Для разбора группировки признаков нужно запоминать больше связей. На этом этапе появляется необходимость в объединении связей в "цепь" друг за другом.


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


Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Полезно убегать от большого и зубастого организма (льва), но не от просто большого (слона) и не от просто зубатого (кролика) Время от времени организм может выполнять действие, но обязательно его выполняет при одновременном наличии признака("1") и признака("2") Запись в цепочку обнаружения сочетания признаков и способ повторного обнаружения такого сочетания признаков Бесконечный цикл со спящим ожиданием одновременной (последовательной) установки флага1 и флага2 для исполнения операции

stage sign chain


Теперь опишем "сложности" признаковой операции И (AND) и особенности их решения методом построения цепочки. Первая и самая главная сложность состоит в том, что если количество базовых признаков у организма $n$, то количество макро-признаков, которые возможно выделить на синхронном парном сочетании из этих признаков $C^2_n = \frac{n!}{{(n-2)!} \cdot 2!}$. Это число больше $n$ (для $3 < n$). И общее число макро-признаков увеличивается с добавлением каждой дополнительной возможности детектировать сочетания базовых признаков. Таких возможностей как:


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

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


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


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


Описанный способ отделения формирования макро-признака от оценки исполненного действия позволяет:


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

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


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


Этап "Группировка действий"


И если признак повторяемости является дискуссионным в биологическом распознавании зрительных образов, то его использование для группировки действий сомнения почти не вызывает (вспомним пример игры на фортепиано, рассмотренный в статье 3). Основа способа формирования алгоритма, использующего последовательность действий, это повтор организмом требуемой последовательности действий! Но о каких действиях мы говорим? Ранее у организма мы рассматривали лишь одно действие, и он учился им пользоваться с пользой для себя. Такое ограничение было использовано намеренно (в угоду простоты описания вышеизложенных шагов развития). Давайте пожалеем организм и снимем это ограничение.


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


Пример эволюционного фактора Универсальное описание Требуемые дополнительные функции Добавляемый тип алгоритма
Для перемещения организма в воде полезно использовать последовательность движения жгутиком (хвостом) сначала в одну и потом в другую сторону Время от времени организм может выполнять последовательность действие("1") и действие("2") и это полезнее чем не выполнять эту последовательность или выполнять действия разрозненно Запись в цепочку сочетания исполняемых действий и способ повторного исполнения такого сочетания действий Бесконечный цикл для исполнения последовательности операции с настраиваемым интервалом

stage action chain


Схема, конечно, красивая. Но.


  1. Каким способом организм подбирает действия к обнаруженной новой ситуации в среде?
  2. Где он хранит оценку полезности (вредности) запомненных действий?
  3. Каким образом он синтезирует цепочку последовательности действий?

Давайте разберем эти "сложности" по порядку.


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


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


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


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


Общая схема памяти завершена?


Выводы


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


  • иерархии в признаковые и исполнительные области памяти;
  • игровой активности;
  • алгоритмов копирования цепочек от организма к организму;
  • языковых единиц для обеспечения исполнения коллективных алгоритмов организмов;
  • использования языковых единиц для процессов копирования цепочек от организма к организму;
  • способов синтеза алгоритмов на основе "цепочек" языковых единиц...

Мы на финальной черте этой статьи. Высадка на остров сокровищ "Алгоритм" произошла. Она получилась длиннее (по количеству букв), чем мне бы этого хотелось, но нас не должно волной прибоя отбросить обратно в море. Поэтому пришлось укрепиться на берегу основательней. С другой стороны количества букв в статье явно не хватает, чтобы описать всё что хочется рассказать, и некоторые подводные камни остались неосвещенными за бортом повествования. Есть предложение выстроить диалог обсуждения этих сложностей в формате вопрос-ответ в отдельной ветке Issues (Open source (GPL) проекта на GitLab).


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


stage action chain


Спасибо Вам за внимание.


Отзывы


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


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


Ссылки


Подробнее..

Ozon go school Как не нужно проводить отбор

26.06.2020 08:09:31 | Автор: admin

Go School


Как вы знаете, в середине мая Ozon объявил о запуске школы программирования на языке Go. Обещали следующее:
  • бесплатное обучение
  • возможность получить знания по реальной разработке на Go от Ozon
  • возможность получить работу в Ozon

Чтобы попасть в школу, нужно было:
  • иметь опыт промышленного программирования
  • пройти тестовые задания по программированию на платформе Яндекс.Контест
  • пройти skype-собеседования

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

Тогда же было озвучено число студентов, которое готовы принять в Школу около 40 человек.

Так понемногу условия поступления прирастали новыми пунктами, среди добавленных также значилось:
  • желательно проживать в Москве
  • быть гражданином РФ
  • возраст старше 18 лет

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

Вроде все выглядело неплохо, условия не такие сложные и вполне выполнимые.



Сперва надо было решить пять задач по программированию на платформе Яндекс.Контест. Первые четыре задания были типичными алгоритмическими задачками, которые обожают давать на собеседованиях. Эти задания можно было решить на любом языке из предложенного списка. Последнее пятое требовалось решать на Go.

Задание E


Описание
Необходимо написать функцию func Merge2Channels(f func(int) int, in1 <-chan int, in2 <-chan int, out chan <-int, n int) в package main.
Описание ее работы:
n раз сделать следующее:
  • прочитать по одному числу из каждого из двух каналов in1 и in2, назовем их x1 и x2.
  • вычислить f(x1) + f(x2)
  • записать полученное значение в out

Функция Merge2Channels должна быть неблокирующей, сразу возвращая управление.
Функция f может работать долгое время, ожидая чего-либо или производя вычисления.

Формат ввода
Количество итераций передается через аргумент n. Целые числа подаются через аргументы-каналы in1 и in2.
Функция для обработки чисел перед сложением передается через аргумент f.

Формат вывода
Канал для вывода результатов передается через аргумент out.

Примечания
Отправлять задачу необходимо под компилятором Make. Решения, выдающие неверный ответ, могут по техническим причинам получать вердикт Runtime Error. Медленные решения получают вердикт Idleness Limit, стоит рассматривать это как превышение времени исполнения.

Сделав все задания, кроме последнего, приступаем к заданию E. Описание задания не казалось сложным, надо было все лишь сделать неблокирующий код функции и возвращать результаты в правильном порядке, но все попытки были неудачными и платформа постоянно выдавала результат WA (Wrong Answer). Хотя на каждое задание отводилось по 100 попыток, тратить их стоило аккуратно, ведь самым бережливым было обещано преимущество при отборе. Два дня я пытался понять в чем дело, израсходовал попыток 20, но все время получал WA.
Решено было создать телеграм-группу с такими же неудачниками и провести брейншторм. Численность группы быстро увеличивалась и чем больше вариантов мы пробовали, тем чаще звучали догадки, что проблема не в решении, а в платформе или проверке.

Тут начинается расследование как работает Яндекс.Контест.
Несколько человек нашли способ смотреть содержимое файлов на платформе через вывод в stderr. Так у нас получилось достать тесты задания (main_test.go), структуру директории и makefile.
Локально тесты проходили, но платформа упорно выдавала WA.
Собранная информация и анализ работы платформы Яндекс.Контест позволили сформулировать алгоритм работы тестов и причину злосчастной ошибки.

Как работают тесты для задания E

Как работают тесты для задания E


По той информации, что нам удалось достать с платформы Яндекс.Контест

Этапы (Make):
1) Компиляция.
1.1) В нашей задаче нет никакой компиляции. Просто два раза перекладывает файл решения.
1.2) Архивация.
На данном этапе уже есть файлы см формат ввода и см формат вывода. Они пустые.

2) Выполнение.
2.1) Разархивация.
2.2) go test. Тут происходит проверка решения. Если оно неверное, то выбрасывает panic. Если верное, то результат запуска go run выводится в STDOUT. Вот это САМЙ важный момент. Запомните это.
Файл запуска
#!/bin/bashgo test


3) Проверка чекером.
Чекер это просто любой скриптовый язык, который на вход принимает три параметра:
input (файл теста), answer (ожидаемый ответ файла теста), output (вывод пользовательского решения на этапе запуска).
Параметры input и output указываются в админке, и в нашем случае они не важны.

Но вот самое интересное это answer. Туда в нашей задаче приходит содержимое файла см формат вывода. НО! Туда никто ничего не пишет и он всегда пустой в нормальном состоянии.
В итоге им всего лишь нужно было перенаправить вывод результата работы go test и проверять вывод чекером.

Так как в файл см формат вывода ничего не записывается, все решения система считает как неверные, и мы получаем результат WA.

Зная, как система работает, удалось создать чит-скрипт. Он писал произвольный символ в файл см формат вывода, из-за чего чекер платформы считал задание выполненным. Фактически это был единственный (хотя и нечестный) способ пройти задание E успешно.

Итого. Если вы не готовы на небольшое мошенничество и отправляете абсолютно корректное решение, то получите только WA.


Ответы от Ozon


После этого исследования несколько человек написало на почту Ozon и представителям школы с подробным описанием багов. На это были получены ответы в духе: У нас все работает, мы видим, что несколько людей выполнили задание. Многие письма с просьбой проверить корректность задания вовсе оставались без ответа.

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


За время контеста Ozon дважды публиковал комментарии, которые могли видеть все участники. Во второй раз там был обширный кусок, посвященный заданию E. В нем сказано много и одновременно ничего:
Проанализировав ваши посылки по задаче Е, мы пришли к выводу, что тест по этой задаче составлен слишком обще и широко. В данном случае одного теста недостаточно. Некоторые пограничные ситуации принимаются как корректные. Ошибка в системе, которая выдается в ответ на решение участника, не всегда предсказуема. Доработать это в режиме реального времени у нас нет возможности: есть риск потерять уже существующие решения. Это мы будем прорабатывать уже после завершения отбора. Сложности связаны также с тем, что Go новый язык для платформы, Make, как компилятор, был добавлен по нашей просьбе, чтобы обработать решения задачи на Go.


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


Сотрудники компании даже были замечены на форуме, где посетители объединились для совместного решения задачи E, грозили им пальчиком и апеллировали к их честности.

Цифры, статистика и печаль

Цифры, статистика и печаль


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

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

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

После того как руководитель школы Ozon не признался, что задание Е сломано и его невозможно пройти, в группе начался бомбеж. Особенно мы не понимали, почему нельзя исправить тест-систему.
Добавил горечи факт того, что брали в школу не тех, кто сделал задания, а тех, кто подходил под критерии отбора. Один из участников группы рассказал, что его знакомого позвали на собеседование, хотя тот сделал лишь два задания из тех четырех, которые можно было пройти без уловок.

17 июня, как и было обещано, Ozon разослал всем письма о результатах (поздно вечером).

Провели несколько опросов, вот результаты:

Возрастной состав группы (131 проголосовавший):
32% 25-29 лет
27% 20-24 лет
24% 30-34 лет
8% 35-40 лет
5% до 19 лет включительно
4% 40+ лет

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

География участников (60 проголосовавших):
35% Москва
20% Приволжский регион
15% Центральный регион
8% Санкт-Петербург
7% Московская область
5% Северо-Западный
3% Южный
2% Украина
3% Белоруссия
2% Казахстан

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

Результаты отбора (100 проголосовавших):
80% получили отказ
12% получили приглашение
8% не пришло ничего

Отказ получали как те, кого не приглашали на собеседование, так и те, кто собеседование прошел.

Каким должен быть отбор здорового человека


1. Рейтинг! Объективный и открытый, в котором указан список тех, кто сделал задания, прошел собесы, кто сколько набрал баллов.

2. Четкие критерии, как начислялись баллы. Должны быть общедоступные критерии, по которым каждый смог бы подсчитать свои баллы и свериться.

3. Почему была допущена ошибка с заданием Е и почему не признали сразу? Что за уловки и попытки уйти от признания своего косяка? Почему сразу не исправили проблему, а упорно говорили о каких-то 32 участниках? Для кого эта лапша? Что за неуважение?

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

Отписки уровня Все работает, все норм, 32 человека решили, потом сломалось, виноват Яндекс, так как мейк у них кривой. Начни с себя, ищи ошибку у себя и т.п. Или отбирали по опыту выполнения тестовых заданий это уж совсем, извините, ни в какие рамки.

Каким получился отбор у Ozon


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

Представьте, если бы вы отправили в ВУЗ результаты ЕГЭ на 399 баллов по четырём предметам и выполнили вступительное на 98, а вам бы в ответе прислали Попробуй еще раз, мало у тебя опыта прохождения отборов вас бы устроил такой вариант? Здесь абсолютно такая же ситуация.

2. Задания не работают, и более того, организаторы до последнего не хотят признавать это, считая своих конкурсантов полными дегенератами размахивая мифическими 32 участниками, прошедшими отбор.

В чем проблема сказать: Да, лоханулись, ща катнем фикс, все заработает.

3. Формальные отписки с отказом. Типа иди получай Опыт прохождения тестовых заданий что, простите?

4. Задания в школу по Go, требующие знаний Go Убила просто постановка задачи Задание можно выполнить только на Go.

5. Молчанка при попытке узнать, написать на почту или в личку. Мы не ожидали 4к людей. А сколько вы ожидали? 15 человек? Достаточно странно ожидать небольшое количество участников и при этом пиарить школу на всех доступных платформах/чатах/каналах в рунете

Заключение


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

Надежда, что Ozon одумается, теплилась до конца. Вечером 23 июня сотрудники компании опубликовали итоги отбора в Школу Go. В посте нашлось место статистике, описанию задач и даже неуклюжему признанию неполадок с заданием E. Но те, кто следил за этой историей, ждали совсем другого признания. Ozon выбрал простой путь: щедро сыпал обвинениями, лишь бы не замараться самому. Вдвойне иронично, что предложенные Ozon решения задания E тоже не проходят тесты на Яндекс.Контест. А ведь уже несколько недель у компании был сделанный нами детальный анализ проблемы. Он так и остался без внимания, найдя свой покой где-то в бездонных корпоративных инбоксах.

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

Ссылки



Спасибо за помощь в написании статьи участников группы Gozone
Подробнее..

Турнирная сортировка

29.06.2020 00:11:43 | Автор: admin

Продолжаем знакомиться с разнообразными кучами и алгоритмами сортировок с помощью этих куч. Сегодня у нас так называемое турнирное дерево.
EDISON Software - web-development
Мы в EDISON часто разрабатываем умные алгоритмы в наших проектах.


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

Мы очень любим computer science ;-)
Основная идея заключается в использовании относительно небольшой (по сравнению с основным массивом) вспомогательной кучи, которая выполняет роль приоритетной очереди. В этой куче сравниваются элементы на нижних уровнях, в результате чего меньшие элементы (в данном случае дерево у нас MIN-HEAP) поднимаются наверх, а в корень всплывает текущий минимум из той порции элементов массива, которые попали в эту кучу. Минимум переносится в дополнительный массив победителей, в результате чего в куче происходит сравнение/перемещение оставшихся элементов и вот уже в корне дерева новый минимум. Отметим, что при такой системе очередной минимум по значению больше чем предыдущий тогда легко собирается новый упорядоченный массив победителей, где новые минимумы просто добавляются в конец дополнительного массива.

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

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

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

  # Библиотека для работы с приоритетной очередью в виде кучи  require_relative "pqueue"  # Максимальный размер для приоритетной очереди  MAX_SIZE = 16  def tournament_sort(array)    # Если массив маленький, то упрощённый "турнир"    return optimal_tourney_sort(array) if array.size <= MAX_SIZE    bracketize(array) # Если массив большой, то стандартный "турнир"  end  # Пропускаем элементы через турнирную сетку  def bracketize(array)    size = array.size    pq = PriorityQueue.new    # Заполняем очередь первыми элементами    pq.add(array.shift) until pq.size == MAX_SIZE    winners = [] # Сбор "победителей"    losers = [] # Сбор "проигравших"    # Пока в основном массиве есть элементы    until array.empty?      # Массив "победителей" пока пуст?      if winners.empty?        # Из корня переносим в массив "победителей"        winners << pq.peek        # Восстановление кучи после переноса корня        pq.remove       end      # Если очередной элемент из массива больше, чем последний "победитель"      if array.first > winners.last        pq.add(array.shift) # Переносим элемент в очередь на вакантное место      else # Если очередной элемент меньше или равен последнему "победителю"        losers << array.shift # Переносим элемент в массив "проигравших"      end      # Если куча пока не пуста, корень переносим в массив "победителей"      winners << pk.peek unless pk.peek.nil?      pq.remove # Восстановление кучи после переноса корня    end    # Основной массив пуст, но может в куче ещё что-то осталось?    until pq.heap.empty?      winners << pq.peek # Корень переносим в массив "победителей"      pq.remove # Восстановление кучи после переноса корня    end    # Если массив "проигравших" остался пуст, то, значит,    # массив "победителей" - итоговый отсортированный массив    return winners if losers.empty?    # Если ещё не закончили, то к массиву "проигравших"    # припишем массив "победителей"    array = losers + winners    array.pop while array.size > size    bracketize(array) # Запускаем процесс снова  end  # Упрощённый турнир если массив меньше чем очередь  def optimal_tourney_sort(array)    sorted_array = [] # Заготовка для отсортированного массива    pq = PriorityQueue.new    array.each { |num| pq.add(num) } # Переносим все элементы в мини-кучу    until pq.heap.empty? # Пока мини-куча не пуста      sorted_array << pq.heap[0]       pq.remove # Восстановление кучи после переноса корня    end    sorted_array # Итоговый массив  end  # Тестирование  if $PROGRAM_NAME == __FILE__    # Генерируем массив    shuffled_array = Array.new(30) { rand(-100 ... 100) }    # Печать необработанного массива    puts "Random Array: #{shuffled_array}"    # Печать обработанного массива    puts "Sorted Array: #{tournament_sort(shuffled_array)}"  end

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


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

Вариант с многопутевым слиянием


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

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


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

Каждый из n элементов массива проходит через турнирное дерево всего один раз, что обходится в O(log n) по времени. В такой реализации алгоритм имеет худшую/среднюю временную сложность O(n log n).

В финале сезона


Серия статей о сортировках кучей почти завершена. Осталось рассказать о самой эффективной из них.

Ссылки


Tournament sort

Priority queue

Tournament sort in Java

The Theory Behind the The Theory Behind the z/Architecture Sort Assist Instructions

Using Tournament Trees to Sort

Tournament Sort Demo Ruby

Tournament Sort Visualization

Tournament Sort Data Structure UGC NET DS

Tournament Sort Algorithm a Heapsort variant

Статьи серии:




В приложение AlgoLab добавлена сегодняшняя сортировка, кто пользуется обновите excel-файл с макросами.

В комментариях к ячейке с название сортировки можно указать некоторые настройки.
Переменная way скольки-путевое турнирное дерево (просто на всякий случай предусмотрена возможность делать это дерево не только двоичным, но и троичным, четверичным и пятиричным).
Переменная queue это размер первоначальной очереди (количество узлов в самом нижнем уровне дерева). Так как деревья полные, то если, к примеру при way=2 указать queue=5, то размер очереди будет увеличен до ближайшей степени двойки (в данном случае до 8).
Переменная NWayMerge принимает значение 1 или 0 с помощью неё указывается, надо ли применять многопутевое слияние или нет.
Подробнее..

Технологические соревнования Радиофест-2020

29.06.2020 22:07:32 | Автор: admin
Мировая пандемия пришла неожиданно и внесла свои коррективы в намеченные планы. Многие мероприятия были отменены, еще больше перенесены и мы также были вынуждены с тревогой следить за развитием ситуации. Находиться в неопределенности и постоянном ожидании очень не просто.

Это был непростой период для всех нас, Covid-19 буквально забрал у нас все то, что, казалось бы, навсегда с нами, привычное общение с коллегами на работе, встречи с друзьями в кафе, прогулки в парке с семьей и многое другое из нашей повседневной жизни. И вот наконец-то жизнь постепенно возвращается в привычное русло. И мы рады Вам сообщить, что Радиофест-2020 состоится! Да, конечно, произошло много изменений и появилось много ограничений. Но, как говориться, нет ничего невозможного. Мы приложим максимальные усилия, чтобы мероприятие прошло комфортно и безопасно.

Начнем с организационной части. Итак, с 7 по 9 октября 2020 года, все в том же замечательно месте, Технополис Москва (площадка Алабушево) состоятся вторые Всероссийские технологические соревнования по перспективным направлениям развития радиосвязи Радиофест-2020. Порядок проведения соревнований также остается неизменным, первые два дня будет хакатон, на котором командам будет предоставлено оборудования для отладки программного обеспечения, третий день торжественное открытие, проведение соревнований по направлениям и церемония награждения победителей.

Теперь про техническую составляющую соревнований. Непосредственно направления проведения соревнований остаются неизменными, это по прежнему радионавигация, радиоперехват и радиосвязь/РЭБ. Но, уже имея опыт проведения первых соревнований, мы внесли некоторые изменения в новый Регламент, направленные, в первую очередь, на расширения аудитории потенциальных участников соревнований. Выбранная нами, для проведения первого радиофеста, SDR платформа LimeSDR, отличный радиочастотный фронтенд, но все же достаточно дорогая, и трудно доставаемая вещь, в амперку или chip-dip не пойдешь, не купишь. Соответственно, далеко не у всех есть возможность приобрести необходимый комплект оборудования и подготовится к соревнованиям. Что ставит тех же радиолюбителей энтузиастов в неравные условия с командами, которых поддерживает образовательное учреждение или тем более коммерческая организация. В связи с этим, в конкурсе поймай волну мы переходим на использование в качестве радиооборудования хорошо себя зарекомендовавшего приемника RTL-SDR Blog V3. Простой, дешевый, легко доставаемый, с достаточным частотным диапазоном, был бы еще канал на передачу, вообще бы цены этому донглу не было. В вычислительной части комплекта оборудования все остается неизменным, используем Raspberry Pi 4 4GB.

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

Итого, на Радиофесте-2020 будут следующие конкурсные задания.

Конкурс по направлению радиоперехват поймай волну не меняет свою суть конкурсное задание заключается в автоматическом определении вида модуляции сигнала. Но вместо LimeSDR командам предоставляется RTL SDR. Если команды показывают одинаковый результат, то для определения победителя проводится Супр-Игра. В ходе Супер-Игры для излучаемого сигнала уменьшается соотношение сигнал шум и добавляется псевдослучайная перестройка рабочей частоты.

Конкурс по направлению радиосвязь/РЭБ захват флага полностью наследуется из прошлогоднего радиофеста. Команды соревнуются между собой, у каждой команды по два полнофункциональных комплекта LimeSDR, задача заключается в организации радиообмена между своими комплектами оборудования в условиях радиоэлектронного противодействия команды соперника. Побеждает команда, передавшая наибольший объем информации.

Конкурс по направлению радионавигация, несмотря на переименование с охоты на лис на геобитва, сути не меняет и заключается в определении координат нескольких источников радиоизлучения с использованием комплектов LimeSDR. Но теперь количество приемников доступных командам фиксировано два комплекта, плюс комплекты оборудования могут обмениваться данными по проводной сети Ethernet. Если команды показываю одинаковый результат, то для определения победителя проводится Супр-Игра. В ходе проведения Супер-Игры один их комплектов оборудования снабжается дополнительной передающей антенной, команды могут использовать передающий канал для постановке помех команде сопернику.

Мероприятию посвящен отдельный веб-сайт. В самое ближайшее время на нем будет опубликован Регламент Радиофест-2020. Также, учитывая опыт проведения предыдущих соревнований, на сайте будет доступен раздел с примерами кода. В частности, будут доступны примеры кода на С и python для взаимодействия с сервером Оргкомитета, в части подключения и загрузки/выгрузки файлов.

Для участи в Радиофест-2020 необходимо заполнить заявку на участие, которая также будет скоро опубликована на сайте http://радиофест.рф

До встречи на Радиофест-2020!
Подробнее..

Перевод Разбираемся в моделях кода архитектуры x64

01.07.2020 18:14:54 | Автор: admin
Какой моделью кода мне воспользоваться? часто возникающий, но нечасто разбираемый вопрос при написании кода для архитектуры х64. Тем не менее, это довольно интересная проблема, и для понимания генерируемого компиляторами машинного кода х64 полезно иметь представление о моделях кода. Кроме того, для тех, кто беспокоится о производительности вплоть до мельчайших команд, выбор модели кода влияет и на оптимизацию.

Информация по этой теме в сети, или где бы то ни было еще, встречается редко. Самым важным из доступных ресурсов является официальный х64 ABI, скачать его можно по ссылке (далее по тексту он будет упоминаться как ABI). Часть информации также можно найти на man-страницах gcc. Задача данной статьи предоставить доступные рекомендации по теме, обсудить связанные с ней вопросы, а так же хорошими примерами через используемый в работе код продемонстрировать некоторые концепты.

Важное замечание: эта статья не является обучающим материалом для начинающих. Перед ознакомлением рекомендуется уверенное владение C и ассемблером, а так же базовое знакомство с архитектурой х64.



Также смотрите нашу предыдущую публикацию на схожую тему: Как x86_x64 адресует память



Модели кода. Мотивационная часть


В архитектуре х64 отсылки и на код, и на данные ведутся через командно-относительные (или, используя жаргон х64, RIP-относительные) модели адресации. В этих командах сдвиг от RIP ограничен 32 битами, однако могут возникнуть случаи, когда команде при попытке адресовать часть памяти или данных попросту не хватит сдвига в 32 бит, например при работе с программами больше двух гигабайт.

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

Таким образом, компромиссом становятся модели кода. [1] Модель кода это формальное соглашение между программистом и компилятором, в котором программист указывает свои намерения относительно размера ожидаемой программы (или программ), в которую попадет компилируемый в данный момент объектный модуль. [2] Модели кода нужны для того чтобы программист мог сказать компилятору: не волнуйся, этот объектный модуль пойдет только в небольшие программы, так что можно пользоваться быстрыми RIP-относительными режимами адресации. С другой стороны, он может сказать компилятору следующее: мы собираемся компоновать этот модуль в большие программы, так что пожалуйста используй неторопливые и безопасные абсолютные режимы адресации с полным 64-битным сдвигом.

О чем расскажет эта статья


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

Каждая из этих моделей кода представлена в независящих друг от друга PIC и не-PIC вариациях, и мы поговорим про каждую из шести.

Исходный пример на С


Для демонстрации обсуждаемых в этой статье концептов я воспользуюсь представленной ниже программой на С и скомпилирую ее с различными моделями кода. Как можно видеть, функция main получает доступ к четырем разным глобальным массивам и одной глобальной функции. Массивы отличаются двумя параметрами: размером и видимостью. Размер важен для объяснения средней модели кода и не понадобится в работе с малой и большой моделями. Видимость важна для работы PIC-моделей кода и бывает либо статичной (видимость только в исходном файле), либо глобальной (видимость всем скомпонованным в программу объектам).

int global_arr[100] = {2, 3};static int static_arr[100] = {9, 7};int global_arr_big[50000] = {5, 6};static int static_arr_big[50000] = {10, 20};int global_func(int param){    return param * 10;}int main(int argc, const char* argv[]){    int t = global_func(argc);    t += global_arr[7];    t += static_arr[7];    t += global_arr_big[7];    t += static_arr_big[7];    return t;}

gcc использует модель кода как значение опции -mcmodel. Кроме того, флагом -fpic можно задать PIC компиляцию.

Пример компиляции в объектный модуль через большую модель кода с использованием PIC:

> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o

Малая модель кода


Перевод цитаты из man gcc на тему малой модели кода:

-mcmodel=small
Генерация кода для малой модели: программа и ее символы должны быть скомпонованы в нижних двух гигабайтах адресного пространства. Размер указателей 64 бит. Программы могут быть скомпонованы и статически, и динамически. Это основная модель кода.


Другими словами, компилятор может спокойно считать, что код и данные доступны через 32-битный RIP-относительный сдвиг из любой команды в коде. Давайте взглянем на дизассемблированный пример программы на С, которую мы скомпилировали через не-PIC малую модель кода:

> objdump -dS codemodel1_small.o[...]int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  4e: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  51: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  57: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  5a: 8b 45 fc                mov    -0x4(%rbp),%eax}  5d: c9                      leaveq  5e: c3                      retq

Как можно видеть, доступ ко всем массивам организован одинаково с использованием RIP-относительного сдвига. Однако в коде сдвиг равен 0, поскольку компилятор не знает, где будет размещен сегмент данных, поэтому для каждого такого доступа он создает релокацию:

> readelf -r codemodel1_small.oRelocation section '.rela.text' at offset 0x62bd8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001500000002 R_X86_64_PC32     0000000000000000 global_func - 4000000000038  001100000002 R_X86_64_PC32     0000000000000000 global_arr + 18000000000041  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004a  001200000002 R_X86_64_PC32     0000000000000340 global_arr_big + 18000000000053  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

Давайте в качестве примера полностью декодируем доступ к global_arr. Интересующий нас дизассемблированный сегмент:

  t += global_arr[7];36:       8b 05 00 00 00 00       mov    0x0(%rip),%eax3c:       01 45 fc                add    %eax,-0x4(%rbp)

RIP-относительная адресация относительна очередной команде, таким образом сдвиг необходимо запатчить в команду mov таким образом, чтобы он соответствовал 0х3с. Нас интересует вторая релокация, R_X86_64_PC32, она указывает на операнд mov по адресу 0x38 и означает следующее: берем значение символа, добавляем слагаемое и вычитаем указываемый релокацией сдвиг. Если вы все корректно посчитали, вы увидите как результат разместит относительный сдвиг между очередной командой и global_arr, плюс 0х1с. Поскольку 0х1с означает седьмой int в массиве (в архитектуре х64 размер каждого int составляет 4 байта), то этот относительный сдвиг нам и нужен. Таким образом, используя RIP-относительную адресацию, команда корректно ссылается на global_arr[7].

Также интересно отметить следующее: пусть команды доступа к static_arr здесь и схожи, его переадресация использует другой символ, тем самым вместо конкретного символа указывая в секцию .data. Виной тому действия компоновщика, он размещает статический массив в известном месте секции, и таким образом массив нельзя использовать совместно с другими общими библиотеками. В итоге компоновщик урегулирует ситуацию с этой релокацией. С другой стороны, поскольку global_arr может быть использован (или перезаписан) другой общей библиотекой, уже динамический загрузчик должен будет разобраться со ссылкой к global_arr. [3]

Наконец, давайте взглянем на отсылку к global_func:

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       e8 00 00 00 00          callq  33 <main+0x1e>33:       89 45 fc                mov    %eax,-0x4(%rbp)

Поскольку операнд callq тоже RIP-относителен, релокация R_X86_64_PC32 работает здесь аналогично размещению фактического относительного сдвига к global_func в операнд.

В заключение отметим, что благодаря малой модели кода компилятор воспринимает все данные и код будущей программы как доступные через 32-битный сдвиг, и тем самым для доступа к всевозможным объектам создает простой и эффективный код.

Большая модель кода


Перевод цитаты из man gcc на тему большой модели кода:

-mcmodel=large
Генерация кода для большой модели: Эта модель не делает предположений относительно адресов и размеров секций.

Пример дизассемблированного кода main, скомпилированного при помощи не-PIC большой модели:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  35: 00 00 00  38: ff d2                   callq  *%rdx  3a: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  3d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  44: 00 00 00  47: 8b 40 1c                mov    0x1c(%rax),%eax  4a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  4d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  54: 00 00 00  57: 8b 40 1c                mov    0x1c(%rax),%eax  5a: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  5d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  64: 00 00 00  67: 8b 40 1c                mov    0x1c(%rax),%eax  6a: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  6d: 48 b8 00 00 00 00 00    movabs $0x0,%rax  74: 00 00 00  77: 8b 40 1c                mov    0x1c(%rax),%eax  7a: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  7d: 8b 45 fc                mov    -0x4(%rbp),%eax}  80: c9                      leaveq  81: c3                      retq

И вновь полезно взглянуть на релокации:

Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000030  001500000001 R_X86_64_64       0000000000000000 global_func + 000000000003f  001100000001 R_X86_64_64       0000000000000000 global_arr + 000000000004f  000300000001 R_X86_64_64       0000000000000000 .data + 1a000000000005f  001200000001 R_X86_64_64       0000000000000340 global_arr_big + 000000000006f  000300000001 R_X86_64_64       0000000000000000 .data + 31080

Поскольку нет необходимости делать предположения относительно размеров секций кода и данных, большая модель кода довольно унифицирована и одинаковым образом определяет доступ ко всем данным. Давайте вновь взглянем на global_arr:

  t += global_arr[7];3d:       48 b8 00 00 00 00 00    movabs $0x0,%rax44:       00 00 0047:       8b 40 1c                mov    0x1c(%rax),%eax4a:       01 45 fc                add    %eax,-0x4(%rbp)

Двум командам необходимо получить желаемое значение из массива. Первая команда размещает абсолютный 64-битный адрес в rax, который, как мы скоро увидим, окажется адресом global_arr, тогда как вторая команда загружает слово из (rax) + 0х1с в eax.

Так что давайте сфокусируемся на команде по адресу 0x3d, movabs, абсолютной 64-битной версии mov в архитектуре х64. Она может забросить полную 64-битную константу прямо в регистр, и так как в нашем дизассемблированном коде значение этой константы равно нулю, за ответом нам придется обратиться к таблице релокаций. В ней мы найдем абсолютную релокацию R_X86_64_64 для операнда по адресу 0x3f, со следующим значением: размещение значения символа плюс слагаемого обратно в сдвиг. Другими словами, rax будет содержать абсолютный адрес global_arr.

А что насчет функции вызова?

  int t = global_func(argc);24:       8b 45 ec                mov    -0x14(%rbp),%eax27:       89 c7                   mov    %eax,%edi29:       b8 00 00 00 00          mov    $0x0,%eax2e:       48 ba 00 00 00 00 00    movabs $0x0,%rdx35:       00 00 0038:       ff d2                   callq  *%rdx3a:       89 45 fc                mov    %eax,-0x4(%rbp)

За уже знакомым нам movabs следует команда call, которая вызывает функцию по адресу в rdx. Достаточно взглянуть на соответствующую релокацию чтобы понять насколько она похожа на доступ к данным.

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

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

Средняя модель кода


Как и ранее, давайте взглянем на перевод цитаты из man gcc:

-mcmodel=medium
Генерация кода для средней модели: Программа скомпонована в нижних двух гигабайтах адресного пространства. Здесь же расположены и малые символы. Символы размера большего, чем задано через -mlarge-data-threshold, попадают в больше данные или секции bss и могут находиться выше двух гигабайт. Программы могут быть скомпонованы и статически, и динамически.

Аналогично малой модели кода, средняя модель предполагает что весь код скомпонован в двух нижних гигабайтах. Тем не менее, данные разделены на предполагаемо скомпонованные в нижних двух гигабайтах малые данные и неограниченные по размещению в памяти большие данные. Данные попадают в категорию больших при превышении ими предела, по определению равного 64 килобайтам.

Также важно отметить, что при работе со средней моделью кода для больших данных по аналогии с секциями .data и .bss создаются специальные секции: .ldata и .lbss. Это не так важно в призме темы текущей статьи, однако я собираюсь немного от нее отклониться. Детальнее с данным вопросом можно ознакомиться в ABI.

Теперь становится понятно, почему в примере появились те массивы _big: они нужны средней модели для интерпретации больших данных, которыми они, при размере в 200 килобайт каждый, и являются. Ниже можно ознакомиться с результатом дизассемблирования:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 48 83 ec 20             sub    $0x20,%rsp  1d: 89 7d ec                mov    %edi,-0x14(%rbp)  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24: 8b 45 ec                mov    -0x14(%rbp),%eax  27: 89 c7                   mov    %eax,%edi  29: b8 00 00 00 00          mov    $0x0,%eax  2e: e8 00 00 00 00          callq  33 <main+0x1e>  33: 89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  3c: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax  45: 01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  48: 48 b8 00 00 00 00 00    movabs $0x0,%rax  4f: 00 00 00  52: 8b 40 1c                mov    0x1c(%rax),%eax  55: 01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  58: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5f: 00 00 00  62: 8b 40 1c                mov    0x1c(%rax),%eax  65: 01 45 fc                add    %eax,-0x4(%rbp)    return t;  68: 8b 45 fc                mov    -0x4(%rbp),%eax}  6b: c9                      leaveq  6c: c3                      retq

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

Средняя модель кода это умелый компромисс между большой и малой моделями. Навряд ли код программы окажется слишком велик [4], так что подвинуть его сверх лимита в два гигабайта могут разве что статически скомпонованные в него большие куски данных, возможно как часть некоего объемного табличного поиска. Так как средняя модель кода отсеивает такие объемные куски данных и особым образом их обрабатывает, то вызовы кодом функций и малых символов будут так же эффективны, как и в малой модели кода. Только обращения к большим символам, по аналогии с большой моделью, потребуют от кода воспользоваться полным 64-битным методом большой модели.

Малая PIC-модель кода


Теперь давайте посмотрим на PIC варианты моделей кода, и как и раньше мы начнем с малой модели. [5] Ниже можно видеть пример кода, скомпилированного через малую PIC-модель:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   48 83 ec 20             sub    $0x20,%rsp  1d:   89 7d ec                mov    %edi,-0x14(%rbp)  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)    int t = global_func(argc);  24:   8b 45 ec                mov    -0x14(%rbp),%eax  27:   89 c7                   mov    %eax,%edi  29:   b8 00 00 00 00          mov    $0x0,%eax  2e:   e8 00 00 00 00          callq  33 <main+0x1e>  33:   89 45 fc                mov    %eax,-0x4(%rbp)    t += global_arr[7];  36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  3d:   8b 40 1c                mov    0x1c(%rax),%eax  40:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr[7];  43:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  49:   01 45 fc                add    %eax,-0x4(%rbp)    t += global_arr_big[7];  4c:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  53:   8b 40 1c                mov    0x1c(%rax),%eax  56:   01 45 fc                add    %eax,-0x4(%rbp)    t += static_arr_big[7];  59:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  5f:   01 45 fc                add    %eax,-0x4(%rbp)    return t;  62:   8b 45 fc                mov    -0x4(%rbp),%eax}  65:   c9                      leaveq  66:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend00000000002f  001600000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000039  001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4000000000045  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b800000000004f  001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 400000000005b  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098

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

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

Интересно обратить внимание на глобальные массивы: стоит напомнить, что в PIC глобальные данные должны проходить через GOT, поскольку в какой-то момент их могут хранить, или пользоваться ими, общие библиотеки [6]. Ниже можно видеть код для доступа к global_arr:

  t += global_arr[7];36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax3d:   8b 40 1c                mov    0x1c(%rax),%eax40:   01 45 fc                add    %eax,-0x4(%rbp)

Интересующая нас релокация это R_X86_64_GOTPCREL: позиция входа символа в GOT плюс слагаемое, минус сдвиг за применение релокации. Другими словами, в команду патчится относительный сдвиг между RIP (следующей инструкции) и зарезервированного для global_arr в GOT слота. Таким образом, в rax в команду по адресу 0x36 размещается фактический адрес global_arr. Следом за этим шагом идет сброс ссылки на адрес global_arr плюс сдвиг на его седьмой элемент в eax.

Теперь давайте взглянем на вызов функции:

  int t = global_func(argc);24:   8b 45 ec                mov    -0x14(%rbp),%eax27:   89 c7                   mov    %eax,%edi29:   b8 00 00 00 00          mov    $0x0,%eax2e:   e8 00 00 00 00          callq  33 <main+0x1e>33:   89 45 fc                mov    %eax,-0x4(%rbp)

В ней есть релокация операнда callq по адресу 0x2e, R_X86_64_PLT32: адрес PLT входа для символа плюс слагаемое, минус сдвиг за применение релокации. Другими словами, callq должен корректно вызывать PLT трамплин для global_func.

Обратите внимание, какие неявные предположения совершает компилятор: что к GOT и PLT можно получить доступ через RIP-относительную адресацию. Это будет важно при сравнении этой модели с другими PIC-вариантами моделей кода.

Большая PIC-модель кода


Дизассемблирование:

int main(int argc, const char* argv[]){  15: 55                      push   %rbp  16: 48 89 e5                mov    %rsp,%rbp  19: 53                      push   %rbx  1a: 48 83 ec 28             sub    $0x28,%rsp  1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx  25: 49 bb 00 00 00 00 00    movabs $0x0,%r11  2c: 00 00 00  2f: 4c 01 db                add    %r11,%rbx  32: 89 7d dc                mov    %edi,-0x24(%rbp)  35: 48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  39: 8b 45 dc                mov    -0x24(%rbp),%eax  3c: 89 c7                   mov    %eax,%edi  3e: b8 00 00 00 00          mov    $0x0,%eax  43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx  4a: 00 00 00  4d: 48 01 da                add    %rbx,%rdx  50: ff d2                   callq  *%rdx  52: 89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  55: 48 b8 00 00 00 00 00    movabs $0x0,%rax  5c: 00 00 00  5f: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  63: 8b 40 1c                mov    0x1c(%rax),%eax  66: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  69: 48 b8 00 00 00 00 00    movabs $0x0,%rax  70: 00 00 00  73: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  77: 01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  7a: 48 b8 00 00 00 00 00    movabs $0x0,%rax  81: 00 00 00  84: 48 8b 04 03             mov    (%rbx,%rax,1),%rax  88: 8b 40 1c                mov    0x1c(%rax),%eax  8b: 01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  8e: 48 b8 00 00 00 00 00    movabs $0x0,%rax  95: 00 00 00  98: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  9c: 01 45 ec                add    %eax,-0x14(%rbp)    return t;  9f: 8b 45 ec                mov    -0x14(%rbp),%eax}  a2: 48 83 c4 28             add    $0x28,%rsp  a6: 5b                      pop    %rbx  a7: c9                      leaveq  a8: c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9
000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0
000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0
00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0
00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0
000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0

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

1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx25: 49 bb 00 00 00 00 00    movabs $0x0,%r112c: 00 00 002f: 4c 01 db                add    %r11,%rbx

Ниже можно прочесть перевод связанной с этим цитаты из ABI:

В малой модели кода ко всем адресам (включая GOT) можно получить доступ через предоставленную архитектурой AMD64 IP-относительную адресацию. Именно поэтому нет нужды в явном указателе GOT и таким образом нет нужды в устанавливающем его прологе в функции. В большой и средней моделях кода необходимо определить регистр для хранения адреса GOT в независимых от расположения объектах, поскольку AMD64 ISA не поддерживает мгновенное перемещение размером больше чем 32 бит.

Давайте посмотрим на то, как описанный выше пролог вычисляет адрес GOT. Во-первых, команда по адресу 0x1e загружает свой собственный адрес в rbx. Затем совместно с релокацией R_X86_64_GOTPC64 совершается абсолютный 64-битный шаг в r11. Эта релокация означает следующее: берем адрес GOT, вычитаем перемещаемый сдвиг и добавляем слагаемое. Наконец, команда по адресу 0x2f складывает оба результата вместе. Итогом становится абсолютный адрес GOT в rbx. [7]

Зачем же мучиться с вычислением адреса GOT? Во-первых, как отмечено в цитате, в большой модели кода мы не можем предполагать, что 32-битного RIP-относительного сдвига будет достаточно для адресации GOT, из-за чего нам и требуется полный 64-битный адрес. Во-вторых, мы все еще хотим работать с PIC-вариацией, так что мы не можем попросту поместить абсолютный адрес в регистр. Скорее сам адрес должен быть вычислен относительно RIP. Для этого и нужен пролог: он совершает 64-битное RIP-относительное вычисление.

В любом случае, раз у нас в rbx теперь есть адрес GOT, давайте посмотрим на то как получить доступ к static_arr:

  t += static_arr[7];69:       48 b8 00 00 00 00 00    movabs $0x0,%rax70:       00 00 0073:       8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax77:       01 45 ec                add    %eax,-0x14(%rbp)

Релокация первой команды это R_X86_64_GOTOFF64: символ плюс слагаемое минус GOT. В нашем случае это относительный сдвиг между адресом static_arr и адресом GOT. Следующая инструкция добавляет результат в rbx (абсолютный адрес GOT) и сбрасывает ссылку со сдвигом по 0x1c. Для простоты визуализации такого вычисления ниже можно ознакомиться с псевдо-C примером:

// char* static_arr// char* GOTrax = static_arr + 0 - GOT;  // rax now contains an offseteax = *(rbx + rax + 0x1c);   // rbx == GOT, so eax now contains                             // *(GOT + static_arr - GOT + 0x1c) or                             // *(static_arr + 0x1c)

Обратите внимание на интересный момент: адрес GOT используется как привязка к static_arr. Обычно GOT не содержит в себе адрес символа, и так как static_arr не является внешним символом, нет причин хранить его внутри GOT. Тем не менее, в данном случае GOT используется как привязка к относительной адресу символа секции данных. Этот адрес, который кроме всего прочего не зависит от расположения, можно найти полным 64-битным сдвигом. Компоновщик в состоянии урегулировать эту релокацию, так что модифицировать секцию кода во время загрузки нет необходимости.

Но что насчет global_arr?

  t += global_arr[7];55:       48 b8 00 00 00 00 00    movabs $0x0,%rax5c:       00 00 005f:       48 8b 04 03             mov    (%rbx,%rax,1),%rax63:       8b 40 1c                mov    0x1c(%rax),%eax66:       01 45 ec                add    %eax,-0x14(%rbp)

Этот код несколько длиннее, а релокация отличается от обычной. По сути, GOT используется здесь более традиционным образом: релокация R_X86_64_GOT64 для movabs лишь говорит функции разместить сдвиг в GOT, там где в rax расположен адрес global_arr. Команда по адресу 0x5f берет адрес global_arr из GOT и помещает его в rax. Следующая команда сбрасывает ссылку на global_arr[7] и помещает значение в eax.

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

  int t = global_func(argc);39: 8b 45 dc                mov    -0x24(%rbp),%eax3c: 89 c7                   mov    %eax,%edi3e: b8 00 00 00 00          mov    $0x0,%eax43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx4a: 00 00 004d: 48 01 da                add    %rbx,%rdx50: ff d2                   callq  *%rdx52: 89 45 ec                mov    %eax,-0x14(%rbp)

Интересующая нас релокация это R_X86_64_PLTOFF64: адрес PLT входа для global_func минус адрес GOT. Результат размещается в rdx, куда затем помещается rbx (абсолютный адрес GOT). В итоге мы получаем адрес PLT входа для global_func в rdx.

Обратите внимание что вновь GOT используется как привязка, на этот раз для обеспечения независящей от адреса отсылки к сдвигу PLT входа.

Средняя PIC-модель кода


Наконец, мы разберем сгенерированный для средней PIC-модели код:

int main(int argc, const char* argv[]){  15:   55                      push   %rbp  16:   48 89 e5                mov    %rsp,%rbp  19:   53                      push   %rbx  1a:   48 83 ec 28             sub    $0x28,%rsp  1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx  25:   89 7d dc                mov    %edi,-0x24(%rbp)  28:   48 89 75 d0             mov    %rsi,-0x30(%rbp)    int t = global_func(argc);  2c:   8b 45 dc                mov    -0x24(%rbp),%eax  2f:   89 c7                   mov    %eax,%edi  31:   b8 00 00 00 00          mov    $0x0,%eax  36:   e8 00 00 00 00          callq  3b <main+0x26>  3b:   89 45 ec                mov    %eax,-0x14(%rbp)    t += global_arr[7];  3e:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  45:   8b 40 1c                mov    0x1c(%rax),%eax  48:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr[7];  4b:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  51:   01 45 ec                add    %eax,-0x14(%rbp)    t += global_arr_big[7];  54:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  5b:   8b 40 1c                mov    0x1c(%rax),%eax  5e:   01 45 ec                add    %eax,-0x14(%rbp)    t += static_arr_big[7];  61:   48 b8 00 00 00 00 00    movabs $0x0,%rax  68:   00 00 00  6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax  6f:   01 45 ec                add    %eax,-0x14(%rbp)    return t;  72:   8b 45 ec                mov    -0x14(%rbp),%eax}  75:   48 83 c4 28             add    $0x28,%rsp  79:   5b                      pop    %rbx  7a:   c9                      leaveq  7b:   c3                      retq

Релокации:

Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:  Offset          Info           Type           Sym. Value    Sym. Name + Addend000000000021  00160000001a R_X86_64_GOTPC32  0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4000000000037  001700000004 R_X86_64_PLT32    0000000000000000 global_func - 4000000000041  001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 400000000004d  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8000000000057  001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4000000000063  000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0

Для начала давайте уберем вызов функции. Аналогично малой модели, в средней модели мы предполагаем, что ссылки на код не превышают пределов 32-битного RIP сдвига, следовательно, код для вызова global_func полностью аналогичен такому же коду в малой PIC-модели, равно как и для случаев массивов малых данных static_arr и global_arr. Поэтому мы сфокусируемся на массивах больших данных, но сначала поговорим о прологе: здесь он отличается от пролога большой модели данных.

1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx

Это весь пролог: чтобы при помощи релокации R_X86_64_GOTPC32 разместить GOT адрес в rbx, потребовалась всего одна команда (по сравнению с тремя в большой модели). В чем же разница? Дело в том, что так как в средней модели GOT не является частью секций больших данных, мы предполагаем его доступность в рамках 32-битного сдвига. В большой модели мы не могли совершать подобные предположения, и были вынуждены пользоваться полным 64-битным сдвигом.

Вызывает интерес тот факт, что код для доступа к global_arr_big похож на такой же код в малой PIC-модели. Это происходит по той же причине, почему пролог средней модели короче пролога большой модели: мы полагаем доступность GOT в рамках 32-битной RIP-относительной адресации. Действительно, к самому global_arr_big нельзя получить такой доступ, но этот случай все равно покрывает GOT, так как фактически global_arr_big в нем и находится, причем в виде полного 64-битного адреса.

Ситуация, тем не менее, отличается для static_arr_big:

  t += static_arr_big[7];61:   48 b8 00 00 00 00 00    movabs $0x0,%rax68:   00 00 006b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax6f:   01 45 ec                add    %eax,-0x14(%rbp)

Этот случай похож на большую PIC-модель кода, поскольку тут мы все же получаем абсолютный адрес символа, которого нет в самом GOT. Так как это большой символ, о котором нельзя предположить его нахождение в нижних двух гигабайтах, нам, как и в большой модели, требуется 64-битный PIC сдвиг.

Примечания:


[1] Не стоит путать модели кода с 64-битными моделями данных и моделями памяти Intel, все это разные темы.

[2] Важно помнить: собственно команды создает компилятор, и режимы адресации фиксируются именно на этом шаге. Компилятор не может знать в какие программы или общие библиотеки попадет объектный модуль, одни могут оказаться малыми, а другие большими. Компоновщику известен размер итоговой программы, но уже слишком поздно: компоновщик может лишь патчить сдвиг команд релокацией, а не менять сами команды. Таким образом, соглашение модели кода должно быть подписано программистом на этапе компиляции.

[3] Если что-то осталось непонятным, ознакомьтесь со следующей статьей.

[4] Тем не менее, объемы постепенно растут. Когда я в последний раз проверял Debug+Asserts билд Clang, он почти достигал одного гигабайта, за что во многом спасибо автогенерируемому коду.

[5] Если вы еще не знаете как работает PIC (как в целом, так и в частности для архитектуры x64), самое время ознакомиться со следующими статьями по теме: раз и два.

[6] Таким образом, компоновщик не может самостоятельно разрешить ссылки, и вынужден переложить обработку GOT на динамический загрузчик.

[7] 0x25 0x7 + GOT 0x27 + 0x9 = GOT



Подробнее..

Перевод - recovery mode Учебный проект на Python алгоритм Дейкстры, OpenCV и UI ( часть 1)

02.07.2020 20:17:59 | Автор: admin
Лабиринты это распространенная головоломка для людей, но они представляют из себя интересную задачу для программирования, которую мы можем решить, используя методы кратчайшего пути, такие как алгоритм Дейкстры.

Вспоминаем алгоритм Дейкстры


Алгоритм Дейкстры один из наиболее популярных алгоритмов теории графов. Он используется для поиска кратчайшего пути между узлами на ориентированном графе. Мы начнем с исходного узла и известных длин ребер между узлами.

Сначала мы присваиваем значение расстояния от источника всем узлам. Узел s получает значение 0, потому что это источник; остальные получают значения для начала.

image

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

image

Узел s теперь завершен (черный), а его соседи a и b приняли новые значения. Новый интересующий узел b, поэтому мы повторяем процесс ослабления соседних узлов b и финализации значения кратчайшего пути для b.

image

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

image

image

image

Наша финальная диаграмма после запуска алгоритма Дейкстры. Числа в каждом узле представляют кратчайшее возможное расстояние от исходного узла.

Концептуализация изображений лабиринта


image

Мы можем представить себе изображение как матрицу пикселей. Каждый пиксель (для простоты) имеет значение RGB 0,0,0 (черный) или 255,255,255 (белый). Наша цель создать кратчайший путь, который начинается на белом и не переходит на чёрные границы. Чтобы представить эту цель, мы можем рассматривать каждый пиксель как узел и рисовать ребра между соседними пикселями с длиной ребер, основанной на разнице значений RGB. Мы будем использовать формулу евклидова квадратного расстояния и добавим 0,1, чтобы гарантировать отсутствие длины пути 0 (требование для алгоритма Дейкстры):

image

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

image

Реализация


Мы можем использовать OpenCV, популярную библиотеку компьютерного зрения для Python, чтобы извлечь значения пикселей и показать наши изображения лабиринта. Давайте также определим координаты нашего начального и конечного местоположений, добавив точки в наш лабиринт

import cv2import matplotlib.pyplot as pltimport numpy as npimg = cv2.imread('maze.png') # read an image from a file usingcv2.circle(img,(5,220), 3, (255,0,0), -1) # add a circle at (5, 220)cv2.circle(img, (25,5), 3, (0,0,255), -1) # add a circle at (5,5)plt.figure(figsize=(7,7))plt.imshow(img) # show the imageplt.show()


image


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

class Vertex:    def __init__(self,x_coord,y_coord):        self.x=x_coord        self.y=y_coord        self.d=float('inf') #current distance from source node        self.parent_x=None        self.parent_y=None        self.processed=False        self.index_in_queue=None


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

def find_shortest_path(img,src,dst):    pq=[] #min-heap priority queue    imagerows,imagecols=img.shape[0],img.shape[1]    matrix = np.full((imagerows, imagecols), None)     #access matrix elements by matrix[row][col]    #fill matrix with vertices    for r in range(imagerows):        for c in range(imagecols):            matrix[r][c]=Vertex(c,r)            matrix[r][c].index_in_queue=len(pq)            pq.append(matrix[r][c])    #set source distance value to 0    matrix[source_y][source_x].d=0    #maintain min-heap invariant (minimum d Vertex at list index 0)    pq = bubble_up(pq, matrix[source_y][source_x].index_in_queue)


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

#Implement euclidean squared distance formuladef get_distance(img,u,v):    return 0.1 + (float(img[v][0])-float(img[u][0]))**2+(float(img[v][1])-float(img[u][1]))**2+(float(img[v][2])-float(img[u][2]))**2#Return neighbor directly above, below, right, and leftdef get_neighbors(mat,r,c):    shape=mat.shape    neighbors=[]    #ensure neighbors are within image boundaries    if r > 0 and not mat[r-1][c].processed:         neighbors.append(mat[r-1][c])    if r < shape[0] - 1 and not mat[r+1][c].processed:            neighbors.append(mat[r+1][c])    if c > 0 and not mat[r][c-1].processed:        neighbors.append(mat[r][c-1])    if c < shape[1] - 1 and not mat[r][c+1].processed:            neighbors.append(mat[r][c+1])    return neighbors


Теперь мы можем реализовать алгоритм Дейкстры и присвоить значения расстояния (d) всем вершинам пикселей в изображении лабиринта:

while len(pq) > 0:    u=pq[0] #smallest-value unprocessed node    #remove node of interest from the queue    pq[0]=pq[-1]     pq[0].index_in_queue=0    pq.pop()    pq=bubble_down(pq,0) #min-heap function, see source code         u.processed=True    neighbors = get_neighbors(matrix,u.y,u.x)    for v in neighbors:        dist=get_distance(img,(u.y,u.x),(v.y,v.x))        if u.d + dist < v.d:            v.d = u.d+dist            v.parent_x=u.x #keep track of the shortest path            v.parent_y=u.y            idx=v.index_in_queue            pq=bubble_down(pq,idx)             pq=bubble_up(pq,idx)


Теперь мы можем вызвать функцию кратчайшего пути и нарисовать решение в нашем лабиринте:

img = cv2.imread('maze.png') # read an image from a file using opencv (cv2) libraryp = find_shortest_path(img, (25,5), (5,220))drawPath(img,p)plt.figure(figsize=(7,7))plt.imshow(img) # show the image on the screen plt.show()


image

image

Давайте попробуем другие лабиринты со всего Интернета.

image

image

image

image

Полный исходный код доступен на GitHub здесь.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


Подробнее..

Восходящая сортировка кучей

03.07.2020 00:15:20 | Автор: admin

Это заключительная статья из серии про сортировки кучей. В предыдущих лекциях мы рассмотрели весьма разнообразные кучные структуры, показывающих отличные результаты по скорости. Напрашивается вопрос: а какая куча наиболее эффективна, если речь идёт о сортировке? Ответ таков: та, которую мы рассмотрим сегодня.
EDISON Software - web-development
Мы в EDISON в наших проектах используем только лучшие методологии разработки.


Когда мы дорабатывали приложения и сайты Московского ювелирного завода мы сделали полный аудит имеющихся веб-ресурсов, переписали их на Python и Django, внедрили SDK для обращения к видеосервису и рассылки SMS-оповещений, произвели интеграцию с системой электронного документооборота и API 2ГИС.

Мы работаем с ювелирной точностью ;-)
Необычные кучи, которые мы рассматривали ранее это, конечно, прекрасно, однако самая эффективная куча стандартная, но с улучшенной просейкой.

Что такое просейка, зачем она нужна в куче и как она работает описано в самой первой части серии статей.

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



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

Итак, как это работает, давайте посмотрим на конкретном примере.

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

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



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

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



Ну и последний шаг используя буферную переменную сдвигаем значения узлов вверх по ветке.



Теперь всё. Восходящая просейка сформировала из массива сортирующее дерево, в котором любой родитель больше чем его потомки.

Итоговая анимация:



Реализация на Python 3.7


Основной алгоритм сортировки ничем не отличается от обычной heapsort:

# Основной алгоритм сортировки кучейdef HeapSortBottomUp(data):    # Формируем первоначальное сортирующее дерево    # Для этого справа-налево перебираем элементы массива    # (у которых есть потомки) и делаем для каждого из них просейку    for start in range((len(data) - 2) // 2, -1, -1):        HeapSortBottomUp_Sift(data, start, len(data) - 1)     # Первый элемент массива всегда соответствует корню сортирующего дерева    # и поэтому является максимумом для неотсортированной части массива.    for end in range(len(data) - 1, 0, -1):         # Меняем этот максимум местами с последним         # элементом неотсортированной части массива        data[end], data[0] = data[0], data[end]        # После обмена в корне сортирующего дерева немаксимальный элемент        # Восстанавливаем сортирующее дерево        # Просейка для неотсортированной части массива        HeapSortBottomUp_Sift(data, 0, end - 1)    return data

Спуск до нижнего листа удобно/наглядно вынести в отдельную функцию:

# Спуск вниз до самого нижнего листа# Выбираем бОльших потомковdef HeapSortBottomUp_LeafSearch(data, start, end):        current = start        # Спускаемся вниз, определяя какой    # потомок (левый или правый) больше    while True:        child = current * 2 + 1 # Левый потомок        # Прерываем цикл, если правый вне массива        if child + 1 > end:             break         # Идём туда, где потомок больше        if data[child + 1] > data[child]:            current = child + 1        else:            current = child        # Возможна ситуация, если левый потомок единственный    child = current * 2 + 1 # Левый потомок    if child <= end:        current = child            return current

И самое главное просейка, сначала идущая вниз, затем выныривающая наверх:

# Восходящая просейкаdef HeapSortBottomUp_Sift(data, start, end):        # По бОльшим потомкам спускаемся до самого нижнего уровня    current = HeapSortBottomUp_LeafSearch(data, start, end)        # Поднимаемся вверх, пока не встретим узел    # больший или равный корню поддерева    while data[start] > data[current]:        current = (current - 1) // 2        # Найденный узел запоминаем,    # в этот узел кладём корень поддерева    temp = data[current]    data[current] = data[start]        # всё что выше по ветке вплоть до корня    # - сдвигаем на один уровень вниз    while current > start:        current = (current - 1) // 2        temp, data[current] = data[current], temp  

На просторах Интернета также обнаружен код на C
/*----------------------------------------------------------------------*//*                         BOTTOM-UP HEAPSORT                           *//* Written by J. Teuhola <teuhola@cs.utu.fi>; the original idea is      *//* probably due to R.W. Floyd. Thereafter it has been used by many      *//* authors, among others S. Carlsson and I. Wegener. Building the heap  *//* bottom-up is also due to R. W. Floyd: Treesort 3 (Algorithm 245),    *//* Communications of the ACM 7, p. 701, 1964.                           *//*----------------------------------------------------------------------*/#define element float/*-----------------------------------------------------------------------*//* The sift-up procedure sinks a hole from v[i] to leaf and then sifts   *//* the original v[i] element from the leaf level up. This is the main    *//* idea of bottom-up heapsort.                                           *//*-----------------------------------------------------------------------*/static void siftup(v, i, n) element v[]; int i, n; {  int j, start;  element x;  start = i;  x = v[i];  j = i << 1;  /* Leaf Search */  while(j <= n) {    if(j < n) if v[j] < v[j + 1]) j++;    v[i] = v[j];    i = j;    j = i << 1;  }  /* Siftup */  j = i >> 1;  while(j >= start) {    if(v[j] < x) {      v[i] = v[j];      i = j;      j = i >> 1;    } else break;  }  v[i] = x;} /* End of siftup *//*----------------------------------------------------------------------*//* The heapsort procedure; the original array is r[0..n-1], but here    *//* it is shifted to vector v[1..n], for convenience.                    *//*----------------------------------------------------------------------*/void bottom_up_heapsort(r, n) element r[]; int n; {  int k;   element x;  element *v;  v = r - 1; /* The address shift */  /* Build the heap bottom-up, using siftup. */  for (k = n >> 1; k > 1; k--) siftup(v, k, n);  /* The main loop of sorting follows. The root is swapped with the last  */  /* leaf after each sift-up. */  for(k = n; k > 1; k--) {    siftup(v, 1, k);    x = v[k];    v[k] = v[1];    v[1] = x;  }} /* End of bottom_up_heapsort */

Чисто эмпирически по моим замерам восходящая сортировка кучей работает в 1,5 раза быстрее, чем обычная сортировка кучей.

По некоторой информации (на странице алгоритма в Википедии, в приведённых PDF в разделе Ссылки) BottomUp HeapSort в среднем опережает даже быструю сортировку для достаточно крупных массивов размером от 16 тысяч элементов.

Ссылки


Bottom-up heapsort

A Variant of Heapsort with Almost Optimal Number of Comparisons

Building Heaps Fast

A new variant of heapsort beating, on an average, quicksort(if n is not very small)

Статьи серии:



В приложение AlgoLab добавлена сегодняшняя сортировка, кто пользуется обновите excel-файл с макросами.
Подробнее..

Сортировка выбором

06.07.2020 02:17:13 | Автор: admin
Всем привет. Эту статью я написал специально к запуску курса Алгоритмы и структуры данных от OTUS.



Введение


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

Постановка задачи


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

Сортировка выбором


Одной из простейших сортировок является сортировка выбором.

Описание алгоритма


Сортировка массива выбором осуществляется так: массив делится на две части. Одна из частей называется отсортированной, а другая неотсортированной. Алгоритм предполагает проход по всему массиву с тем, чтобы длина отсортированной части стала равна длине всего массива. В рамках каждой итерации мы находим минимум в неотсортированной части массива и меняем местами этот минимум с первым элементом неотсортированной части массива. После чего мы увеличиваем длину отсортированной части массива на единицу. Пример осуществления одной итерации представлен ниже:
1 3 5 | 8 9 6 -> 1 3 5 6 | 9 8

Реализация


Предлагаю посмотреть на реализацию данного алгоритма на языке C:
void choiseSortFunction(double A[], size_t N){    for(size_t tmp_min_index = 0; tmp_min_index < N;                                  tmp_min_index++) {        //ищем минимум        for(size_t k = tmp_min_index + 1; k < N; k++) {            if (A[k] < A[tmp_min_index]) {                double min_value = A[k];                A[k] = A[tmp_min_index];                A[tmp_min_index] = min_value;            }        }    }}


Анализ


Предлагаю проанализировать данный алгоритм. Тело внутреннего цикла само по себе выполняется за O(1), то есть не зависит от размера сортируемого массива. Это означает, что для понимания асимптотики алгоритма необходимо посчитать сколько раз выполняется это тело. На первой итерации внешнего цикла имеют место (n 1) итераций внутреннего цикла. На второй итерации внешнего цикла (n 2) итерации внутреннего цикла. Продолжая это рассуждение и далее, мы приходим к следующему:

$T(n) = (n - 1) * O(1) + (n - 2) * O(1) + ... + O(1) = O((n - 1) + (n - 2) + ... + 1) = O((n - 1) * n / 2) = O(n ^ 2)$



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

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

$M(n) = O(1)$



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

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

Двусторонняя сортировка выбором


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

Рекурсивная сортировка выбором


В качестве упражнения можно попробовать написать алгоритм не с использованием цикла, а с использованием рекурсии. На java это может выглядеть следующим образом:
public static int findMin(int[] array, int index){    int min = index - 1;    if(index < array.length - 1) min = findMin(array, index + 1);    if(array[index] < array[min]) min = index;    return min;}  public static void selectionSort(int[] array){    selectionSort(array, 0);}  public static void selectionSort(int[] array, int left){    if (left < array.length - 1) {        swap(array, left, findMin(array, left));        selectionSort(array, left+1);    }}  public static void swap(int[] array, int index1, int index2) {    int temp = array[index1];    array[index1] = array[index2];    array[index2] = temp;}


Итоги


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



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


Подробнее..

2.МАТЕМАТИЧЕСКОЕ ОПИСАНИЕ СИСТЕМ АВТОМАТИЧЕСКОГО УПРАВЛЕНИЯ

29.06.2020 02:20:27 | Автор: admin

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


2.1. Получение уравнений динамики системы. Статическая характеристика. Уравнение динамики САУ (САР) в отклонениях
2.2. Линеаризация уравнений динамики САУ (САР)
2.3. Классический способ решения уравнений динамики


Лекции по курсу Управление Техническими Системами, читает Козлов Олег Степанович на кафедре Ядерные реакторы и энергетические установки, факультета Энергомашиностроения МГТУ им. Н.Э. Баумана. За что ему огромная благодарность.


Данные лекции только готовятся к публикации в виде книги, а поскольку здесь есть специалисты по ТАУ, студенты и просто интересующиеся предметом, то любая критика приветствуется.


Первая часть: Введение в теорию автоматического управления. Основные понятия теории управления техническим системами




2.1. Получение уравнений динамики системы. Статическая характеристика. Уравнение динамики САУ (САР) в отклонениях


При составлении уравнений, описывающих нестационарные процессы в САУ (САР) и которые в дальнейшем будем называть уравнениями динамики, система разбивается на отдельные элементы (звенья), для каждого из которых не существует проблем в записи соответствующего уравнения динамики.


На рис. 2.1.1 представлено схематичное представление САУ (звена) в переменных вход-выход, где x(t) (или u(t)) входное воздействие, а y(t) выходное воздействие, соответственно. Нередко входное воздействие будет называться управляющим, а выходное воздействие регулируемой величиной (переменной).



Рис. 2.1.1 Схематическое представление САУ (звена)

При составлении уравнений динамики используются фундаментальные законы сохранения из разделов Механики, Физики, Химии и др.


Например, при описании перемещения узла какого-то механизма силового привода используются законы сохранения: момента, энергии, импульса и др В теплофизических (теплогидравлических) системах используются фундаментальные законы сохранения: массы (уравнение неразрывности), импульса (уравнение движения), энергии (уравнение энергии) и др


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


$\left[ \begin{gathered} y(t) =y_0 + \Delta y(t)\\ u(t) = u_0 + \Delta u(t)\\ \end{gathered} \right.$


где: $y_0,u_0$ стационарные значения входного и выходного воздействий;
$\Delta y, \Delta u$ отклонения от станционара, соотвесвенно.

В качестве примера рассмотрим технологию получения уравнений динамики для механического демпфера, схематическое изображение которого представлено на рис. 2.1.2.



Рис. 2.1.2 Механический демпфер

Согласно 2-му закону Ньютона, ускорение тела пропорционально сумме сил, действующих на тело:

$m \cdot \frac{d^2 y(t)}{dt} = \sum F_j \ \ \ \mathbf{(2.1.1)}$


где, m масса тела, Fj все силы воздействующие на тело (поршень демпфера)


Подставляя в уравнение (2.1.1) все силы согласно рис. 2.2, имеем:


$m \cdot \frac{d^2y(t)}{dt} = m\cdot g+ u(t) - k \cdot y(t) - c \cdot \frac{dy(t)}{dt} \ \ \ \mathbf{(2.1.2)}$

где $m\cdot g$ сила тяжести; $k \cdot y(t)$ сила сопротивления пружины, $c \cdot \frac{dy(t)}{dt}$ сила вязконо трения (пропорциональна скорости поршеня)


Размерности сил и коэффициентов, входящих в уравнение (2.1.2):

$m \cdot g \Rightarrow [\frac{кг \cdot м}{ с^2}]; c \Rightarrow [\frac{кг}{с}]; k \Rightarrow [\frac{кг}{с^2}]$

Предполагая, что при t 0 поршень демпфера находился в равновесии, то есть

$$display$$\left \{ \begin{eqnarray} t &\le 0 \\ u(t) &= u_0\\ y(t) &= y_0\\ \end{eqnarray} \right.$$display$$

перейдем к отклонениям от стационарного состояния:
Пусть при t>0 $\left[ \begin{gathered} y(t) =y_0 + \Delta y(t); u(t) = u_0 + \Delta u(t);\\ y'(t) = [\Delta y(t)]; y''(t) =[\Delta y(t)]'\\ \end{gathered} \right.$. Тогда, подставляя эти соотношения в уравнение (2.1.2), получаем:

$m \cdot \frac{d^2\Delta y(t)}{dt^2} = m \cdot g - k \cdot y_0- k \cdot \Delta y(t) - c\frac{d\Delta y(t)}{dt} \ \ \ \mathbf{(2.1.3)}$


если $t \le 0$, то уравнение принимает вид:

$0 = m \cdot g+u_0 - k \cdot y_0 \ \ \ \mathbf{(2.1.4)}$

или

$y_0 = \frac{1}{k} \cdot[u_0+m \cdot g] \ \ \ \mathbf{(2.1.5)}$


Соотношение (2.1.4) уравнение звена (демпфера) в равновесном (стационарном) состоянии, а соотношение (2.1.5) статическая характеристика звена демпфера (см. рисунок 2.1.3).



Рис. 2.1.3 Статическая характеристика механического демпфера

Вычитая из уравнения (2.1.3) уравнение (2.1.4), получаем уравнение динамики демпфера в отклонениях:

$m\cdot g \frac{d^2 \Delta y(t)}{dt^2} = \Delta u(t) - k \cdot \Delta y(t) - c \cdot \frac{d \Delta y(y)}{dt}, $


тогда, разделив на k, имеем:

$T_2^2 \cdot \frac{d^2 \Delta y(t)}{dt^2}+ T_1 \cdot \frac{d \Delta y(t)}{dt} + \Delta y(t) = k_1 \cdot \Delta u(t) \ \ \ \mathbf{(2.1.6)}$


где:

$T_2^2= \frac{m}{k}; T_1= \frac{c}{k}; k_1=\frac{1}{k}.$


Уравнение (2.1.6) это уравнение динамики в канонической форме, т.е. коэффициент при y(t) равен 1.0!


Легко видеть, что коэффициенты перед членами, содержащими производные, имеют смысл (и размерность!) постоянных времени. В самом деле:


$T_1 = \frac{c}{k} \Rightarrow [\frac{кг \cdot c^2}{c \cdot кг}] = [c]$


$T_2^2 = \frac{m}{k} \Rightarrow [\frac{кг \cdot c^2}{kg}] =[c^2]$


Таким образом, получаем, что:
коэффициент перед первой производной имеет размерность [c] т.е. смысл некоторой постоянной времени;
коэффициент перед второй производной: [$c^2$];
коэффициент в правой части ($k_1$): [$\frac{c^2}{кг}$].
Тогда уравнение (2.1.6) можно записать в операторной форме:


$(T_2^2 \cdot p^2 + T_1 \cdot p+1)\Delta y(t) = k_1 \Delta u(t)$, что эквивалентно

$L(p)\Delta y(t) = N(p) \Delta u(t) \ \ \ \mathbf{(2.1.6.а)}$

где: $p= \frac{d}{dt}$ оператор диффренцирования;
$L(p) = T_2^2 \cdot p + T \cdot p + 1$ -линейный дифференциальный оператор; $L(p)$
$N(p)$ линейный дифференциальный оператор, вырожденный в константу, равную $k_1$.

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


Введем новые нормированные (безразмерные) переменные:

$\left[ \begin{gathered} \widetilde y(t) = \frac {y(t)-y_0}{y_0} = \frac {\Delta y(t)}{y_0}\\ \widetilde u(t) = \frac {u(t)-u_0}{u_0} =\frac {\Delta u(t)}{u_0}\\ \end{gathered} \right. \Rightarrow \left[ \begin{gathered} y(t) =y_0 \cdot [1+ \widetilde y(t)]\\ u(t) = u_0 \cdot [1+ \widetilde u(t)]\\ \end{gathered} \right. \Rightarrow \left[ \begin{gathered} y'(t) =y_0 \cdot \widetilde y(t)\\ y''(t) = y_0 \cdot \widetilde y(t)''\\ \end{gathered} \right. $


Подставляя эти соотношения в уравнение (2.1.2), имеем:

$m \cdot y_0 \cdot \frac{d^2 \widetilde y(t)}{dt^2} = m \cdot g +u_0 \cdot[1+\widetilde u(t)] -k \cdot y_0 \cdot[1+\widetilde y(t)] - c \cdot y_0 \cdot \frac{d \widetilde y(t)}{dt}; или$

$m \cdot y_0 \cdot \frac{d^2 \widetilde y(t)}{dt^2} = \underline {m \cdot g} + \underline {u_0} +u_0 \cdot \widetilde u(t) - \underline {k \cdot y_0} - k \cdot y_0 \cdot \widetilde y(t) - c \cdot y_0 \cdot \frac{d \widetilde y(t)}{dt}.$


Поддчеркнутые члены выражения в сумме дают 0 (см. 2.1.4) Перенося в левую часть члены, содержащие $\widetilde y(t)$, и, разделив на $k \cdot y_0$, получаем:

$\frac{m \cdot y_0}{k \cdot y_0} \cdot \frac{d^2 \widetilde y(t)}{dt^2} + \frac {c \cdot y_0}{k \cdot y_0} \cdot \frac{d \widetilde y(t)}{dt} +\widetilde y(t)= \frac{u_0}{k \cdot y_0} \cdot \widetilde u(t) \Rightarrow $

$(T_2^2 \cdot p + T \cdot p + 1) \cdot \widetilde y(t) = k_x \cdot \widetilde u(t) \ \ \ \mathbf{(2.1.7)}$


где: $T_2^2= \frac{m}{k}; T_1= \frac{c}{k}; k_x=\frac{u_0}{k \cdot y_0} $ коэффициент усиления, причем безразмерный.

Проверим размерность коэффициента $k_x$

$\left[\frac{кг \cdot м}{с^2} \cdot \frac{c^2}{кг} \cdot \frac{1}{м}\right] =[1].$


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


На рис. 2.1.4 представлены статические характеристики для механического демпфера:



Рис. 2.1.4 Статические характеристики механического демпфера

Процедура нормировки отклонений позволяет привести уравнения динамики к виду:

$L(p) \cdot \widetilde y(t) = N(p) \cdot \widetilde u(t) \ \ \ \mathbf{(2.1.8)}$

где $L(p), N(p) - $ дифференциальные операторы.

Если дифференциальные операторы $L(p), N(p) - $ линейные, а статическая характеристика САУ (звена) тоже линейна, то выражение (2.1.8) соответствует линейному обыкновенному дифференциальному уравнению (ОДУ).


А если $L(p), N(p) - $ нелинейные дифференциальные операторы, или $\widetilde y_{stat} \neq k \cdot \widetilde u_{stat}$, то уравнение динамики нелинейное. Под нелинейными действиями понимаются все математические действия, кроме сложения (+) и вычитания (-).


Пример создания модели демпфера можно посмотереть здесь: Технология получения уравнений динамики ТАУ



2.2. Линеаризация уравнений динамики САУ (САР)


Практически все реальные системы автоматического управления (САУ) являются нелинейными, причем нелинейность САУ может определяться различным причинами:


  1. Нелинейностью статической характеристики.
  2. Нелинейностью динамических членов в уравнениях динамики.
  3. Наличием в САУ принципиально нелинейных звеньев.

Если в замкнутой САУ (САР) нет принципиально нелинейных звеньев, то в большинстве случаев уравнения динамики звеньев, входящих в систему, могут быть линеаризованы. Линеаризация основана на том, что в процессе регулирования (т.е. САУ с обратной связью) все регулируемые величины мало отклоняются от их программных значений (иначе система регулирования или управления не выполняла бы своей задачи).


Например, если рассмотреть управление мощностью энергетического ядерного реактора, то главная задача САР поддержание мощности на заданном (номинальном) уровне мощности. Существующие возмущения (внутренние и внешние) отрабатываются САР и поэтому параметры ядерного реактора незначительно отличаются от стационарных. На рис. 2.2.1 представлена временная зависимость мощности ядерного реактора, где нормированные отклонения мощности N /N0 << 1, и поэтому уравнения динамики ядерного реактора, в принципе, могут быть линеаризованы.



Рис. 2.2.1 Пример изменения мощности реактора

Рассмотрим некоторое звено (или САР в целом), описание динамики которого можно представить в переменных вход-выход:



Рис. 2.2.2 Звено САР

Предположим, что динамика данного звена описывается обыкновенным дифференциальным уравнением n-го порядка:

$L(p) \cdot y(t) = N(p) \cdot u(t) \ \ \ \mathbf{(2.2.1)}$


Перенесем $N(p) u(t)$ в левую часть уравнения и запишем уравнение в виде%

$F(y,y',y'',...y^{n}, u, u',u'', ...u^m,t)=0 \ \ \ \mathbf{(2.2.2)}$


где $F$ - функция регулируемой переменной и ее производных, а также управляющего (входного) воздействия и его производных, причем F обычно нелинейная функция.


Будем считать, что при t 0 САУ (звено) находилось в равновесии (в стационарном состоянии). Тогда уравнение (2.2.2) вырождается в уравнение статической характеристики:

$y'=y''=...y^n=u'=u''=...u^m=0 \Rightarrow F(y_0,u_0)=0 \ \ \ \mathbf{(2.2.3)}$


Разложим левую часть уравнения (2.2.2) в ряд Тейлора в малой окрестности точки равновесного состояния $(y_0, u_0)(y0, u0)$.


Напомним, что разложение в ряд Тейлора трактуется следующим образом: если $y = f(x)$, то простое разложение функции в ряд Тейлора в окрестности точки $x = x_0$ будет выглядеть так:


$ \begin{eqnarray} y(x) = f(x) = f(x_0)+ \frac{1}{1!} \cdot \left( \frac{df}{dx}\right)_{x=x_0} \cdot (x-x_0)+ \frac{1}{2!} \cdot \left( \frac{d^2f}{dx^2} \right)_{x=x_0} \cdot (x-x_0) +\\ ...+\frac{1}{n!} \cdot \left( \frac{d^nf}{dx^n} \right)_{x=x_0} \cdot (x-x_0) \end{eqnarray}$


C учетом вышеприведенного разложение принимает вид:


$ \begin{eqnarray} F(y,y',y'',...y^{n}, u, u', ...u^m,t) = F(y_0,u_0)+\frac{1}{1!}\cdot \left( \frac{dF}{dy}\right)_{ y=y_0;u =u_0 } \cdot (y-y_0)+\\ + \frac{1}{2!} \cdot \left( \frac{d^2F}{dy^2}\right)_{y=y_0;u =u_0}\cdot (y-y_0)^2+\frac{1}{3!} \cdot \left( \frac{d^3F}{dy^3}\right)_{y=y_0;u =u_0}\cdot (y-y_0)^3 +..\\+\frac{1}{k!} \cdot \left( \frac{d^kF}{dy^k}\right)_{y=y_0;u =u_0}\cdot (y-y_0)^k+ \frac{1}{1!}\cdot \left( \frac{dF}{dy'}\right)_{ 0 } \cdot (y') +\frac{1}{2!}\cdot \left( \frac{d^2F}{d(y')^2}\right)_{ 0 } \cdot (y')^2+\\ ..+\frac{1}{1!}\cdot \left( \frac{dF}{dy^{(n)}}\right)_{ 0 } \cdot (y^{(n)})+ \frac{1}{2!}\cdot \left( \frac{d^2F}{d(y^{n})^2}\right)_{ 0 } \cdot (y^{(n)})^2+..\\ ..+\frac{1}{1!}\cdot \left( \frac{dF}{du}\right)_{ 0 } \cdot (u-u_0)+\frac{1}{2!} \cdot \left( \frac{d^2F}{du^2}\right)_{0}\cdot (u-u_0)^2+..\\ ..+\frac{1}{k!}\cdot \left( \frac{d^kF}{d(u^{m})^k}\right)_{ 0 } \cdot (u^{(m)})^k+.. \end{eqnarray}$


Предполагая, что отклонения выходных и входных воздействий незначительны, (т.е.:$\frac{\Delta y(t)}{y_0} <<1; \frac{\Delta u(t)}{u_0} <<1;$), оставим в разложении только члены первого порядка малости (линейные). Поскольку $[y(t)-y_0] \equiv \Delta y(t); y'(t) = [y_0+\Delta y(t)]' = (\Delta y(t))'$, получаем:


$ F(y,y',y''..y^{(n)},u,u',u''..u^{m})\simeq \left( \frac{\partial F}{\partial y} \right)_0\cdot \Delta y+\left(\frac{\partial F}{\partial y'}\right)_0 \cdot (\Delta y)' +\left(\frac{\partial F}{\partial y''}\right)_0 \cdot (\Delta y)''+..\\ ..+\left(\frac{\partial F}{\partial u}\right)_0 \cdot \Delta u+ \left(\frac{\partial F}{\partial u'}\right)_0 \cdot (\Delta u)' + \left(\frac{\partial F}{\partial u''}\right)_0 \cdot (\Delta u)''+..+ \left(\frac{\partial F}{\partial u^{m}}\right)_0 \cdot (\Delta u)^{(m)}\ \ \ \mathbf{(2.2.4)}$


Подставляя соотношение (2.2.4) в уравнение (2.2.2), и перенося множители при у и u в разные части получаем уравнения:

$a_n^0 \cdot \frac{d^{(n)}}{dt^n} \Delta y +a_{n-1}^0 \cdot \frac{d^{(n-1)}}{dt^{n-1}} \Delta y +..+a_{1}^0 \cdot \frac{d}{dt} \Delta y +a_{0}^0\cdot\Delta y = \\ =b_m^0 \cdot \frac{d^{(m)}}{dt^m} \Delta u +b_{m-1}^0 \cdot \frac{d^{(m-1)}}{dt^{m-1}} \Delta u +..+b_{1}^0 \cdot \frac{d}{dt} \Delta u +b_{0}^0\cdot\Delta u \ \ \ \mathbf{(2.2.5)}$

где:

$a_0^0= \left( \frac{\partial F}{\partial y} \right)_{y =y_0;u =u_0};a_1^0= \left( \frac{\partial F}{\partial y'} \right)_{y =y_0;u =u_0}; ...a_n^0= \left( \frac{\partial F}{\partial y^{(n)}} \right)_{y =y_0;u =u_0} ; \\ b_0^0= \left( \frac{\partial F}{\partial u} \right)_{y =y_0;u =u_0};b_1^0= \left( \frac{\partial F}{\partial u'} \right)_{y =y_0;u =u_0}; ...b_m^0= \left( \frac{\partial F}{\partial u^{(m)}} \right)_{y =y_0;u =u_0}.$


Коэффициенты $a_i^0, b_j^0$ постоянные коэффициенты, поэтому уравнения 2.2.5 линейное дифференциальное с постоянными коэффициентами.


В дальнейшем нами будет часто использоваться операторная форма записи уравнений динамики:

$L(p)\cdot\Delta y(t) = N(p) \cdot \Delta u(t)\ \ \ \mathbf{(2.2.6)}$


где $p = \frac{\partial }{\partial t}$ оператор дифференцирования;
$L(p)$ линейный дифференциальный оператор степени n;
$N(p)$ линейный дифференциальный оператор степени m, причем обычно порядок оператора $L(p)$ выше порядка оператора $N(p)$: $ n m.$

Уравнения (2.2.5) и (2.2.6) уравнения динамики системы (звена) в отклонениях.


Если исходное уравнение (2.2.1) дифференциальное уравнение в физических переменных (температура, скорость, поток и т.д.), то размерность коэффициентов $a_i^0, b_j^0$ может быть произвольной (любой).


Переход к нормализованным отклонениям позволяет упорядочить размерность коэффициентов. В самом деле, разделив уравнение (2.2.5) на $(y_0, u_0)$ и выполнив некоторые преобразования, получаем:


$a_n^* \cdot \tilde{y}^{(n)} +a_{(n-1)}^* \cdot \tilde{y}^{(n-1)} +..+a_1^* \cdot \tilde{y}'+a_0^* \cdot \tilde{y} = \\ =b_m^* \cdot \tilde{u}^{(m)} +b_{(m-1)}^* \cdot \tilde{u}^{(m-1)} +..+b_1^* \cdot \tilde{u}'+b_0* \cdot \tilde{u} \ \ \ \mathbf{(2.2.7)}$


Приведение уравнения динамики САУ (звена) к нормализованному виду позволяет унифицировать размерность коэффициентов уравнений: ==>


$$display$$[a_0^*] = [1] ;\ \ [a_1^*]= [c];\ \ [a_2^*]= [c^2];\ \ [a_3^*]= [c^3];...[a_n^*]= [c^n]\\ [b_0^*] = [1] ;\ \ [b_1^*]= [c];\ \ [b_2^*]= [c^2];\ \ [b_3^*]= [b^3];...[b_m^*]= [c^m]$$display$$


Если вынести в правой части (2.2.7) коэффициент $b_0^*$ за общую скобку и разделить все уравнение на $a_0^*$, то уравнение принимает вид:


$a_n \cdot \tilde{y}^{(n)} +a_{(n-1)}\cdot \tilde{y}^{(n-1)} +..+a_1\cdot \tilde{y}'+\tilde{y} = \\ =k \cdot [b_m \cdot \tilde{u}^{(m)} +b_{(m-1)} \cdot \tilde{u}^{(m-1)} +..+b_1 \cdot \tilde{u}'+ \tilde{u}] \ \ \ \mathbf{(2.2.8)}$


где:

$a_n = \frac{a_n^*}{a_0^*};\ a_{n-1} = \frac{a_{n-1}^*}{a_0^*}; \ ...a_{1} = \frac{a_{1}^*}{a_0^*}; \ k = \frac{b_0^*}{a_0^*} \\ b_n = \frac{b_n^*}{b_0^*};\ b_{n-1} = \frac{b_{n-1}^*}{b_0^*}; \ ...b_{1} = \frac{b_{1}^*}{b_0^*}; $

или в операторном виде:

$(a_n \cdot p^{(n)} +a_{(n-1)}\cdot p^{(n-1)} +..+a_1\cdot p'+1) \cdot \tilde{y} = \\ =k \cdot (b_m \cdot p^{(m)} +b_{(m-1)} \cdot p^{(m-1)} +..+b_1 \cdot \tilde{u}'+ 1)\cdot \tilde{u}\\ L(p)\cdot \tilde{y} =k \cdot N(p) \cdot \tilde{u} \ \ \ \mathbf{(2.2.9)}$


Линеаризация уравнений динамики и нормализация переменных позволяют привести уравнения динамики САУ (звена) к виду, наиболее удобному для использования классических методов анализа, т.е. к нулевым начальным условиям.


$$display$$Если \ t 0 \Rightarrow \left[ \begin{gathered} \tilde {y}(t) = \tilde {y}(0) =0;\\ \tilde u(t) = \tilde u(0) = 0.\\ \end{gathered} \right.$$display$$


Пример


Выполнить линеаризацию уравнения динамики некоторой абстрактной САР в окрестности состояния (x0, y0), если полное уравнение динамики имеет вид:


$a_3^0 \cdot y'''(t) +a_2^0 \cdot y''(t)+a_1^0 \cdot y'(t) \cdot[y(t)-d]+ a_2^0 \cdot y^2(t)=b_1^0 \cdot x'(t) +b_0^0 \cdot x(t); $


Нелинейность полного уравнения динамики проявляется в следующем:


во-первых, в нелинейности статической характеристики:


$a_0^0 \cdot y^2(0) = b_0^0 \cdot x(0); \Rightarrow y_0 = \sqrt{\frac{b_0^0}{a_0^0} \cdot x_0} = \sqrt {k_0 \cdot x_0}$



Рис. 2.2.3 Линеаризации статической характеристики

во-вторых, слагаемое в левой части $a_1^0 \cdot y'(t) \cdot[y(t)-d]$ чисто нелинейное, так как действие умножения является нелинейным.


Выполним процесс линеаризации исходного уравнения, динамики без разложения я ряд Тейлора, основываясь на том, что в окрестности состояния (x0, y0) нормированные отклонения управляющего воздействия и регулируемой величины намного меньше 1.


Преобразования выполним в следующей последовательности:


  1. Перейдем к безразмерным переменным (нормализованным);
  2. Выполним линеаризацию, отбросив нелинейные члены 2-го и выше порядков малости.

Перейдем к новым безразмерным переменным:


$\tilde{y}(t) = \frac{y(t) - y_0}{y_0};\ \Rightarrow y(t) = y_0 \cdot [1+ \tilde{y}(t)]; \\ \tilde{x}(t) = \frac{x(t) - x_0}{x_0};\ \Rightarrow x(t) = x_0 \cdot [1+ \tilde{x}(t)].$


Заметим, что:
$x(t) = x_0+ \Delta x(t) = x_0+ x_0 \cdot \tilde{x}(t) \ и \ y(t) = y_0+ \Delta y(t) = y_0+ y_0 \cdot \tilde{y}(t)$.


Подставляя значения x(t) и y(t) в исходное уравнение:


$ a_3^0 \cdot y_0 \cdot \tilde y'''(t) +a_2^0 \cdot y_0 \cdot \tilde y''(t)+a_1^0 \cdot y_0 \cdot \tilde y'(t) \cdot[y_0+y_0 \cdot \tilde y(t) -d]+a_0^0 \cdot y_0^2 \cdot[1+ \tilde y(t)]^2 = \\ = b_1^0 \cdot x_0 \cdot \tilde x'(t) + b_0^0 \cdot х_0 \cdot[1+\tilde x(t)]; \ \Rightarrow \ раскрыв \ скобки \Rightarrow \\ a_3^0 \cdot y_0 \cdot \tilde y'''(t)+a_2^0 \cdot y_0 \cdot \tilde y''(t)+a_1^0 \cdot y_0^2 \cdot \tilde y'(t) + a_1^0 \cdot y_0^2 \cdot \tilde y'(t) \cdot \tilde y(t)-a_1^0 \cdot y_0 \cdot \tilde y'(t) \cdot d + \\ +a_0^0 \cdot y_0^2 +2 \cdot a_0^0 \cdot y_0^2 \cdot\tilde y(t) +a_0^0 \cdot y_0^2 \cdot\tilde y(t)^2 = b_1^0 \cdot x_0 \cdot \tilde x'(t) + b_0^0 \cdot х_0+ b_0^0 \cdot х_0 \cdot \tilde x(t)];$


Удаляем полученного уравнения уравнения стационара: $a_0^0 \cdot y_0^2= b_0^0 \cdot х_0$, а так же пренебрегая слагаемыми второго прядка малости: $\ a_1^0 \cdot y_0^2 \cdot \tilde y'(t) \cdot \tilde y(t)$, получаем следующее уравнение:


$ a_3^0 \cdot y_0 \cdot \tilde y'''(t)+a_2^0 \cdot y_0 \cdot \tilde y''(t)+(a_1^0 \cdot y_0^2 -a_1^0 \cdot y_0 \cdot d) \cdot \tilde y'(t)+2 \cdot a_0^0 \cdot y_0^2 \cdot\tilde y(t) =\\ = b_1^0 \cdot x_0 \cdot \tilde x'(t) + b_0^0 \cdot х_0 \cdot \tilde x(t);$


Вводим новые обозначения:

$a_3^* = a_3^0 \cdot y_0 ; \ \ a_2^* = a_2^0 \cdot y_0; \ \ a_1^* = a_1^0 \cdot y_0^2 -a_1^0 \cdot y_0 \cdot d; \ \ a_0^* =2 \cdot a_0^0 \cdot y_0^2; \\ b_1^* = b_1^0 \cdot x_0; \ \ b_0^*=b_0^0 \cdot х_0 \ $


Получаем уравнения в почти классическом виде:


$a_3^* \cdot \tilde y'''(t)+a_2^* \cdot \tilde y''(t)+a_1^* \cdot \tilde y'(t)+ a_0^* \cdot\tilde y(t) = b_1^* \cdot \tilde x'(t) + b_0^* \cdot \tilde x(t);$


Если в правой части вынести за общую скобку $b_0^*$ и разделить все уравнение на $a_0^*$, то уравнение (линеаризованное) принимает вид:


$a_3 \cdot \tilde y'''(t)+a_2 \cdot \tilde y''(t)+a_1 \cdot \tilde y'(t)+ \tilde y(t) = k \cdot[ b_1 \cdot \tilde x'(t) + \tilde x(t)]$


где:

$a_3 = \frac{a_3^*}{a_0^*}; \ \ a_2 = \frac{a_2^*}{a_0^*}; \ \ a_1 = \frac{a_2^*}{a_0^*}; \ \ k = \frac{b_o^*}{a_0^*}; \ \ b_1 = \frac{b_1^*}{b_0^*}; $


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


2.3. Классический способ решения уравнений динамики


Классический метод решения уравнений динамики САУ (САР) применим только для линейных или линеаризованных систем.


Рассмотрим некоторую САУ (звено), динамика которой описывается линейным дифференциальным уравнением вида:


$L(p) \cdot y(t) = N(p)*x(t), \ \ \ \mathbf{(2.3.1)}\\ где: L(p) = a_n\cdot p^n + ..+a_1 \cdot p + a_0 \\ N(p) = b_m \cdot p^m+..+b_1 \cdot p+b_0 $


Переходя к полной символике, имеем: $\Rightarrow$


$a_n \cdot y^{(n)}+a_{n-1} \cdot y^{(n-1)}+..+a_1 \cdot y'+a_{0} = b_m \cdot x^{(m)}+b_{n-1} \cdot x^{(n-1)}+..+b_1 \cdot y'+b_{0} \ \ \mathbf{(2.3.2)};$


Выражение (2.3.2) обыкновенное дифференциальное уравнение (ОДУ), точнее неоднородное ОДУ, так как правая часть 0.


Известно входное воздействие x(t), коэффициенты уравнения и начальные условия (т.е. значения переменных и производных при t = 0).


Требуется найти y(t) при известных начальных условиях.


Известно, что


$y(t) = y_{общ.}(t)+y_{част.}(t),\ \ \ \mathbf{(2.3.3)}$


где: $y_{общ.}(t)$ решение однородного дифференциального уравнения $L(p) y(t) = 0; $y_{част.}(t) $inline$ - частное решение. $inline$


Будем называть решение однородного дифференциального уравнения $y_{общ.} = y_{собств.}$, собственным решением, так как его решение не зависит от входного воздействия, а полностью определяется собственными динамическими свойствами САУ (звена).


Вторую составляющую решения (2.3.3) будем называть $y_{част.} = y_{вын.}$, вынужденным, так как эта часть решения определяется внешним воздействием $x(t)$, поэтому САУ (САР или звено) вынуждена отрабатывать это воздействие:


$y(t) = y_{собств.}(t)+y_{вын.}(t),\ \ \ \mathbf{(2.3.4)}$


Напомним этапы решения:


1) Если имеется уравнение вида $L(p) \cdot y(t) = N(p)*x(t)$, то сначала решаем однородное дифференциальное уравнение:


$a_n \cdot y^{(n)}+a_{n-1} \cdot y^{(n-1)}+..+a_1 \cdot y'+a_{0} =0 $


2) Записываем характеристическое уравнение:


$L(\lambda) =0; \ \Rightarrow \ a_n \cdot \lambda^{n}+a_{n-1} \cdot \lambda^{n-1}+..+a_1 \cdot \lambda+a_{0} =0 \ \ \ \mathbf{(2.3.5)}$


3) Решая уравнение (2.3.5), которое является типичным степенным уравнением, каким-либо способом (в том числе и с помощью стандартных подпрограмм на компьютере) находим корни характеристического уравнения $\lambda_i$
4) Тогда собственное решение записывается в виде:


$y_{собств.}(t)=\sum_{j=1}^n C_j \cdot e^{\lambda_j \cdot t}, \ \ \ \mathbf{(2.3.6)}$


если среди $\lambda_i$ нет повторяющихся корней (кратность корней равна 1).


Если уравнение (2.3.5) имеет два совпадающих корня, то собственное решение имеет вид:


$y_{собств.}(t)=\sum_{j=1}^{n-2} C_j \cdot e^{\lambda_{j} \cdot t} +C_{n-1} \cdot e^{\lambda_{n-1} \cdot t}\cdot [1+C_n\cdot t]. \ \ \ \mathbf{(2.3.7)}$


Если уравнение (2.3.5) имеет k совпадающих корней (кратность корней равна k), то собственное решение имеет вид:


$y_{собств.}(t)=\sum_{j=1}^{n-k} C_j \cdot e^{\lambda_{j} \cdot t} +C_{n+1-k} \cdot e^{\lambda_{n+1-k} \cdot t}\cdot [1+C_{n+2-k}\cdot t+C_{n+3-k}\cdot t^2+.. \\ ..+C_{n}\cdot t^{k-1}]. \ \ \ \mathbf{(2.3.8)}$


5) Вынужденную часть решения можно найти различными способами, но наиболее распространены следующие способы:
а) По виду правой части.
б) Методом вариации постоянных.
в) Другие методы


Если вид правой части дифференциального уравнения относительно несложная функция времени, то предпочтительным является способ а): подбор решения. $y_{вын.}(t) = f_{вын}(t)$.


6) Суммируя полученные составляющие (собственную и вынужденную), имеем: $\Rightarrow$


$y_{полн.}(t)=\sum_{j=1}^n C_j \cdot e^{\lambda_j \cdot t} + f_{вын}(t).$


7) Используя начальные условия (t = 0), находим значения постоянных интегрирования $C_j$. $\Rightarrow$ Обычно получается система алгебраических уравнений. $\Rightarrow$ Решая систему, находим значения постоянных интегрирования $C_j$


Пример


Найти аналитическое выражение переходного процесса на выходе звена, если


$\left\{ \begin{gathered} 2 \cdot y''(t)+5 \cdot y'(t)+2 \cdot y(t) =1- e^{-t}\\ Начальные \ условия \ t = 0; \Rightarrow y(0) = 0; y'(0) =0.\ \end{gathered} \right.$


Решение. $\Rightarrow$ Запишем однородное ОДУ: $2 \cdot y''(t)+5 \cdot y'(t)+2 \cdot y(t) =0$
Характеристическое уравнение имеет вид: $2 \cdot \lambda ^2+5 \cdot \lambda+2 = 0$; Решая, имеем: $\lambda_1 = -2; \ \ \lambda = 0.5,$ тогда:

$y_{соб} = С_1 \cdot e^{-2 \cdot t}+С_2 \cdot e^{-0.5 \cdot t},$


где $С_1, С_2$ неизвестные (пока) постоянные интегрирования.

По виду временной функции в правой части запишем $y_{вын}(t)$ как:


$у_{вын}(t) =A+B \cdot e^{-t} \Rightarrow у_{вын}'(t) = -B\cdot e^{-t} \Rightarrow у_{вын}''(t) = B\cdot e^{-t} $


Подставляя в исходное уравнение, имеем:


$2\cdot B \cdot e^{-t} - 5 \cdot B \cdot e^{-t}+2\cdot A +2 \cdot B \cdot e^{-t} =1 - e^{-t} \Rightarrow \left\{ \begin{gathered} 2 \cdot A =1\\ -B = -1\ \end{gathered} \right. \Rightarrow\\ \Rightarrow \left\{ \begin{gathered} A = \frac{1}{2}\\ B = 1\ \end{gathered} \right. \Rightarrow y_{вын.}(t) = \frac{1}{2} -e^{-t};$


Суммируя $y_{соб}, y_{вын}$, имеем: $y(е) = С_1 \cdot e^{-2 \cdot t}+С_2 \cdot e^{-0.5 \cdot t}+\frac{1}{2}+ e^{-t}.$


Используя 1-е начальное условие (при t = 0), получаем: $0 =C_1+C_2+0.5+1$, а из 2-го начального условия имеем: $0 = -2 \cdot C_1 - 0.5 \cdot C_2 -1.$


Решая систему уравнений относительно $С_1$ и $С_2$, имеем: $С_1 = -1/6; \ \ \ C_2 = -4/3. $
Тогда окончательно:


$y(t) = - \frac{1}{6} e^{-2t}- \frac{4}{3} e^{-0.5t}+\frac{1}{2}+e^{-t};$


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


$2 \cdot y''(t)+5 \cdot y'(t)+2 \cdot y(t) =1- e^{-t} \Rightarrow \\ \Rightarrow y''(t) = 0.5 - 0.5\cdot e^{-t} - 2.5 \cdot y'(t)- y(t)$


Создадим модель SimInTech, содержащую исходное динамическое уравнение и полученное аналитическое решение, и выведем результаты на один график (см. рис. 2.3.1).



Рис. 2.3.1 структурная схема для проверки решения


На рис. 2.3.2 приведено решение по вышеприведенному соотношению и численное решение задачи в среде SimInTech (решения совпадают и линии графиков наложены друг на друга).



Рис. 2.3.2 Решение уравнения динамики

Ссылки по теме:


  1. Википедия про ряд Тейлора
  2. Дифференциальные уравнения на Match24.ru
  3. Пример создания модели груза на пружине.
  4. Начало лекций здесь: Введение в теорию автоматического управления. Основные понятия теории управления техническим системами
Подробнее..

Из песочницы Хеш-таблицы

02.07.2020 14:05:51 | Автор: admin

Предисловие


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


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


image


Мотивация использовать хеш-таблицы


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


Контейнер \ операция insert remove find
Array O(N) O(N) O(N)
List O(1) O(1) O(N)
Sorted array O(N) O(N) O(logN)
Бинарное дерево поиска O(logN) O(logN) O(logN)
Хеш-таблица O(1) O(1) O(1)

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


Понятие хеш-таблицы


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


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


Теперь стало понятно, почему же это именно хеш-таблица.


Проблема коллизии


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


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


Решения проблемы коллизии методом двойного хеширования


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


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


Мы будем рассматривать сначала элемент s, потом s + t, затем s + 2*t и т.д. Естественно, чтобы не выйти за границы массива, мы обязаны смотреть на номер элемента по модулю (остатку от деления на размер массива).


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




Реализация хеш-таблицы


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


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)        hash_result = (key * hash_result + s[i]) % table_size;    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1); // ключи должны быть взаимопросты, используем числа <размер таблицы> плюс и минус один.    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};

Чтобы идти дальше, нам необходимо разобраться с проблемой: что же будет, если мы удалим элемент из таблицы? Так вот, его нужно пометить флагом deleted, но просто удалять его безвозвратно нельзя. Ведь если мы так сделаем, то при попытке найти элемент (значение хеш-функции которого совпадет с ее значением у нашего удаленного элемента) мы сразу наткнемся на пустую ячейку. А это значит, что такого элемента и не было никогда, хотя, он лежит, просто где-то дальше в массиве. Это основная сложность использования данного метода решения коллизий.


Помня о данной проблеме построим наш класс.


template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8; // начальный размер нашей таблицы    constexpr static const double rehash_size = 0.75; // коэффициент, при котором произойдет увеличение таблицы    struct Node    {        T value;        bool state; // если значение флага state = false, значит элемент массива был удален (deleted)        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr; // соответственно в массиве будут хранится структуры Node*    int size; // сколько элементов у нас сейчас в массиве (без учета deleted)    int buffer_size; // размер самого массива, сколько памяти выделено под хранение нашей таблицы    int size_all_non_nullptr; // сколько элементов у нас сейчас в массиве (с учетом deleted)};

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


...public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr; // заполняем nullptr - то есть если значение отсутствует, и никто раньше по этому адресу не обращался    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }

Из необходимых методов осталось еще реализовать динамическое увеличение, расширение массива метод Resize.
Увеличиваем размер мы стандартно вдвое.


void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value); // добавляем элементы в новый массив        }        // удаление предыдущего массива        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Немаловажным является поддержание асимптотики O(1) стандартных операций. Но что же может повлиять на скорость работы? Наши удаленные элементы (deleted). Ведь, как мы помним, мы ничего не можем с ними сделать, но и окончательно обнулить их не можем. Так что они тянутся за нами огромным балластом. Для ускорения работы нашей хеш-таблицы воспользуемся рехешом (как мы помним, мы уже выделяли под это очень странные переменные).


Теперь воспользуемся ими, если процент реальных элементов массива стало меньше 50 %, то мы производим Rehash, а именно делаем то же самое, что и при увеличении таблицы (resize), но не увеличиваем. Возможно, это звучит глуповато, но попробую сейчас объяснить. Мы вызовем наши хеш-функции от всех элементов, переместим их в новых массив. Но с deleted-элементами это не произойдет, мы не будем их перемещать, и они удалятся вместе со старой таблицей.


Но к чему слова, код все разъяснит:


void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        // удаление предыдущего массива        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Ну теперь мы уже точно на финальной, хоть и длинной, и полной колючих кустарников, прямой. Нам необходимо реализовать вставку (Add), удаление (Remove) и поиск (Find) элемента.


Начнем с самого простого метод Find элемент по значению.


bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size); // значение, отвечающее за начальную позицию        int h2 = hash2(value, buffer_size); // значение, ответственное за "шаг" по таблице        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true; // такой элемент есть            h1 = (h1 + h2) % buffer_size;            ++i; // если у нас i >=  buffer_size, значит мы уже обошли абсолютно все ячейки, именно для этого мы считаем i, иначе мы могли бы зациклиться.        }        return false;    }

Далее мы реализуем удаление элемента Remove. Как мы это делаем? Находим элемент (как в методе Find), а затем удаляем, то есть просто меняем значение state на false, но сам Node мы не удаляем.


bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }

Ну и последним мы реализуем метод Add. В нем есть несколько очень важных нюансов. Именно здесь мы будем проверять на необходимость рехеша.


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


bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash(); // происходит рехеш, так как слишком много deleted-элементов        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1; // запоминаем первый подходящий (удаленный) элемент        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false; // такой элемент уже есть, а значит его нельзя вставлять повторно            if (!arr[h1]->state && first_deleted == -1) // находим место для нового элемента                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1) // если не нашлось подходящего места, создаем новый Node        {            arr[h1] = new Node(value);            ++size_all_non_nullptr; // так как мы заполнили один пробел, не забываем записать, что это место теперь занято        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size; // и в любом случае мы увеличили количество элементов        return true;    }

В заключение приведу полную реализацию хеш-таблицы:


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)    {        hash_result = (key * hash_result + s[i]) % table_size;    }    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1);    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8;    constexpr static const double rehash_size = 0.75;    struct Node    {        T value;        bool state;        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr;    int size;    int buffer_size;    int size_all_non_nullptr;    void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }    void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr;    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }    bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash();        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false;            if (!arr[h1]->state && first_deleted == -1)                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1)        {            arr[h1] = new Node(value);            ++size_all_non_nullptr;        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size;        return true;    }    bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }    bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true;            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }
Подробнее..

Перевод Быстрый поиск без индекса

03.07.2020 00:15:20 | Автор: admin

Проблема



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

Конечно, они также хотели получить данные быстро. Я, как обычно, сказал: Я посмотрю, что я могу сделать и пошел поближе взглянуть на обсуждаемую таблицу. Удача никогда н покинет нас, индекс действительно не существовал, и таблица была огромной. Не проблема, мы можем просканировать таблицу, правильно? Неправильно. Если я чему-то научился за годы работы с базами данных, то этот размер имеет значение. Таблица с сотнями миллионов записей, состоящая из нескольких целочисленных столбцов, была бы достаточно грозной. Затем добавьте различные столбцы varchar и datetime. Теперь это настоящий вызов, не так ли?



Таблица, на которую я смотрел, имела миллиард записей, буквально. Простое сканирование таблицы могло легко занять больше суток, и мне нужно было также обработать извлеченные данные. Кроме того, сканирование таблицы такого размера может оказаться не таким уж благоприятным для общего состояния системы, как кажется на первый взгляд. Все отсканированные страницы должны быть извлечены с дисков в память sql сервера, заполнив её. Это, в свою очередь, выгрузит из кеша страницы данных, которые могут быть использованы для текущих оперативных задач. Запросы ваших текущих пользователей могут ждать дольше, пока сервер перечитывает данные с дисков, вместо того, чтобы быстро повторно использовать страницы данных из памяти. Производительность может снизиться, и в худшем случае сервер может быть полностью поставлен на колени. Таким образом, при сканировании таблицы следует использовать особую технику запускать ее небольшими партиями, сохраняя где-то текущую позицию и промежуточные результаты. Такой подход позволяет серверу перенастраиваться и иметь передышку, не допуская большой просадки производительности.

Поиск



Для всех кто, как и я, закончил обучение довольно давно, переподготовка по теории должна быть в порядке вещей. Бинарный поиск это простой, но чрезвычайно эффективный способ поиска значения в отсортированном массиве (в нашем случае в столбце таблицы). Массив должен быть предварительно отсортирован и конечен. Поиск осуществляется путем сравнения среднего элемента вашего массива с целью. Если они равны, то поиск окончен. Если нет, вы удаляете половину, в которой искомый элемент заведомо отсутствует, и повторяете предыдущий шаг пока не останется один. Найдите середину, сравните цель с ней, если они не равны, снова уберите половину и так далее. Общее количество шагов, необходимых для нахождения цели в худшем случае, когда вы найдете ее к самому последнему шагу, будет log(N), где N число элементов. Сравните это с N / 2, когда вы просто перебираете массив. Вообще говоря, это было бы N, но, если бы мы могли догадаться, ближе ли цель к концу, мы могли бы вернуться назад, таким образом уменьшая общее количество необходимых шагов.

В моем случае, N=1,000,000,000 и вот как я пришел к двум числам выше: 30 и 500,000,000. Идентификатор (ID) играл бы роль перечислителя массива, а datetime создания был бы значением элемента массива. Хотя здесь есть одна оговорка. Перечислитель массива, по самому определению, является непрерывной последовательной последовательностью целых чисел. В идентификаторах таблиц легко могут быть пробелы, из-за удаления записи или повторного заполнения идентификатора. Простое определение середины путем деления диапазона идентификаторов на 2, не следует ожидать, что там будет запись с таким идентификатором. Вместо прямого поиска мне пришлось использовать функцию top (). Что-то вроде этого:

Select top(1) * from Table where id <= @id order by id desc


Я использовал <= и desc потому что я хотел найти значение либо равное или непосредственно перед целью. При условии @l, @r это левая и правая границы соответственно, id это середина, @dt это время создания (creation datetime), tdt это цель и idr реальный идентификатор таблицы (ID), алгоритм может выглядеть следующим образом:

while @l <@rbegin -- найти середину @id = @l +floor((@r-@l)/2) -- найти запись в таблице select top(1) @idr = id, @dt = creation_datetime from Table where id <= @id order by id desc -- переместить границу if(@dt<@tdt) @l = @id +1 else @r = @idend 


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

Написание и тестирование скрипта заняли у меня около часа. Используя его, я нашел первую запись с определенной датой и временем создания. После этого я использовал простой оператор select с предложением where, которое содержало оба условия: id >= @ and creation_datetime >= @dt1 and creation_datetime < @dt2. Мне нужно было только убедиться, что оптимизатор выбрал бы использование первичного ключа вместо индекса или сканирования таблицы. В общем, я сделал это менее чем за 2 часа! Было удивительно вновь обнаружить, что алгоритмы не являются чем-то эзотерическим, похороненным глубоко внутри sql сервера, а скорее тем, что можно легко использовать в повседневной работе.

Ниже весь скрипт, который я написал. Пожалуйста, смотрите комментарии внутри, чтобы понять, как его использовать.
/* Запустите этот скрипт на вашей бд Он найдет значение, равное или непосредственно перед целью. Обратите внимание, что точность datetime ограничена 3 мс*/-- флаг отладки, если установлен, он будет показывать результаты для каждой итерацииdeclare @debug bit = 0-- @Table - имя таблицы, в которой вы хотите искать.declare @Table varchar(128) = 'TableX' -- @Id - имя столбца идентификатора (id) для таблицы declare @ID varchar(128) = 'Id' -- @DateTimeColumn - имя вашего datetime столбца со временем создания в таблицеdeclare @DateTimeColumn varchar(128) = 'created_dt'-- это целевое значение даты и времениdeclare @TargetDateTime datetime = '2020-01-03 18:23:03'declare @Sql varchar(max)set @sql = '-- это ваш отладочный флагdeclare @debug bit = <debug>-- это ваше целевое значениеdeclare @tdt datetime = ''<TargetDateTime>''-- в этой переменной мы сохраняем промежуточное значение (результат деления) declare @id bigint -- это ваши левая и правая границы соответственноdeclare @l bigint, @r bigint-- это переменные, в которых мы храним результаты текущего шага поиска, datetime и идентификатор таблицы соответственноdeclare @dt datetime, @idr bigint-- найти первые левые и правые значенияselect @l = min(<ID>), @r = max(<ID>) from <Table>while @l < @rbegin -- ожидаемый идентификатор set @id = @l +floor((@r-@l)/2) -- найти значение и идентификатор записи select top(1) @dt = <DateTimeColumn>, @idr = <ID> from <Table> where <ID> <= @id order by <ID> desc -- если требуется отладка, покажите текущие значения if( @debug = 1 ) begin select ''@id'' = @id, ''@l'' = @l, ''@r'' = @r, ''@dt'' = @dt, ''@idr'' = @idr end if(@dt < @tdt) set @l = @id +1 else set @r = @idend-- проверьте, есть ли у соседних записей лучшее совпадениеdeclare @t table(id bigint, dt datetime, dlt float)insert @t(id, dt, dlt)select top(1) <ID>, <DateTimeColumn>, abs(cast(<DateTimeColumn> as float) -cast(@tdt as float)) from <Table> where <ID> < @idr order by <ID> descinsert @t(id, dt, dlt) select top(1) <ID>, <DateTimeColumn>, abs(cast(<DateTimeColumn> as float) -cast(@tdt as float)) from <Table> where <ID> > @idr order by <ID>insert @t(id, dt, dlt) select @idr, @dt, abs(cast(@dt as float) -cast(@tdt as float))select top(1) @dt = dt, @idr = id from @t order by dlt, id select ''Found Value'' = @dt, ''Record Id'' = @idr'set @sql = replace( replace( replace( replace( replace(@sql, '<ID>', @ID), '<Table>', @Table), '<TargetDateTime>', convert(varchar(50), @TargetDateTime, 121)), '<debug>', @debug), '<DateTimeColumn>', @DateTimeColumn)exec (@sql)
Подробнее..

Game of Life с битовой магией, многопоточностью и на GPU

03.07.2020 20:16:17 | Автор: admin

Всем привет!


Недавняя статья на Хабре в очередной раз показала неостывающий интерес к игре Жизнь в частности и всевозможным оптимизациям в общем. Статья и комментарии к ней, особенно любопытство к вычислениям на GPU, вдохновили меня на то, чтобы поделиться своими изысканиями на данном поприще и, забегая вперёд, скажу, что повествование пойдёт о расчётах на GPU, битовой магии, многопоточности и огромных полях для игры Жизнь, порядка миллиарда клеток.



О себе


Пару слов о себе и проекте. Вот уже несколько лет моим хобби и pet-проектом является написание игры Жизнь, в ходе которого я разбираюсь в интересующих меня темах. Так, сперва я сделал её с помощью библиотеки glut и прикрутил многопользовательский режим, будучи вдохновлённым архитектурой peer-to-peer. А около двух лет назад решил начать всё с нуля, используя Qt и вычисления на GPU.

Масштабы


Идея нового проекта заключалась в том, чтобы сделать кроссплатформенное приложение, в первую очередь для мобильных устройств, в котором пользователи смогут окунуться в игру Жизнь, познакомиться с разнообразием всевозможных паттернов, наблюдать за причудливыми формами и комбинациями этой игры, выбрав скорость от 1 до 100 мс на шаг и задав размер игрового поля вплоть до 32'768 x 32'768, то есть 1'073'741'824 клетки.

На всякий случай напомню об игре Жизнь. Игра происходит на плоскости, поделённой на множество клеток. Клетки квадратные, соответственно, для каждой клетки есть 8 соседей. Каждая клетка может быть либо живой, либо мёртвой, то есть пустой. В рамках игры пользователь заполняет клетки жизнью, выставляя на поле заинтересовавшие его комбинации клеток паттерны.
Далее процесс шаг за шагом развивается по заданным правилам:
  • в пустой клетке, рядом с которой ровно 3 живые клетки, зарождается жизнь
  • если у живой клетки есть 2 или 3 живых соседки, то эта клетка продолжает жить
  • если у живой клетки меньше 2 или больше 3 живых соседки, клетка умирает

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

В моей реализации поле зациклено, то есть после 32'767-й клетки вновь следует 0-я. Таким образом, перемещающиеся паттерны способны обогнуть поле и оказаться в точке, где они были размещены изначально. Забавный эффект случается с паттерном планёрное ружьё, когда выпущенные им планёры в конечном итоге врезаются в само ружьё и разрушают его, этакое самоубийство в мире клеточных автоматов:

Gosper glider gun


Архитектура


Что касается реализации, то в первую очередь хочется отметить, что каждая клетка в игровом поле представляет собой всего лишь 1 бит в памяти. Так, при выборе размера игрового поля в памяти выделяется буфер размером n^2 / 8 байт. Это улучшает локальность данных и снижает объём потребляемой памяти, а главное позволяет применить ещё более хитроумные оптимизации, о которых поговорим ниже. Игровое поле всегда квадратное и его сторона всегда степени 2, в частности затем, чтобы без остатка осуществлять деление на 8.

Архитектурно выделяются два слоя, ответственные за вычисления:
  • низкий уровень платформозависимый; на текущий момент существует реализация на Metal API графическое API от Apple позволяет задействовать GPU на устройствах от Apple, а также fallback-реализация на CPU. Одно время существовала версия на OpenCL. Планируется реализация на Vulkan API для запуска на Android
  • высокий уровень кроссплатформенный; по сути класс-шаблонный метод, делегирующий реализацию нужных методов на низкий уровень

Низкий уровень


Задача низкого уровня заключается непосредственно в расчёте состояния игрового поля и устроена следующим образом. В памяти хранятся два буфера n^2 / 8 байт. Первый буфер служит как input текущее состояние игрового поля. Второй буфер как output, в который записывается новое состояние игрового поля в процессе расчётов. По завершении вычислений достаточно сделать swap буферов и следующий шаг игры готов. Такая архитектура позволяет распараллелить расчёты в силу константности input. Дело в том, что каждый поток может независимо обрабатывать клетки игрового поля.

За счёт многопоточности GPU-подход добивается максимальной эффективности. На CPU также происходит распараллеливание, но, соответственно, с гораздо меньшим числом потоков и меньшей эффективностью, что отражено в секции бенчмарков ниже. Сам алгоритм идентичен для всех реализаций и заключается в следующем.

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

[........]


А участок игрового поля с интересующим нас байтом и его восемью соседними байтами изобразим так:

[........][........][........]
[........][********][........]
[........][........][........]


Исходя из рисунка, можно догадаться, что все соседние клетки можно уложить в один uint64_t (назовём его neighbours), а ещё в одном uint8_t (назовём его self), будет храниться информация о соседях в самом обрабатываемом байте.

Рассмотрим на примере расчёт 0-го бита целевого байта. Звёздочкой отметим интересующий бит, а нулём соседние для него биты:

[.......0][00......][........]
[.......0][*0......][........]
[.......0][00......][........]


Пример посложнее. Здесь звёздочкой отмечены 0-й, 3-й и 7-й биты целевого байта и соответствующими числами соседние биты:

[.......0][00333.77][7.......]
[.......0][*03*3.7*][7.......]
[.......0][00333.77][7.......]


Думаю, кто-то из читателей уже догадался, в чём заключается магия. Имея указанную информацию, остаётся лишь составить битовые маски для каждого бита целевого байта и применить их к neighbours и self соответственно. Результатом станут 2 значения, сумма единичных бит которых будет характеризовать число соседей, что можно интерпретировать как правила игры Жизнь: 2 или 3 бита для поддержания жизни в живой клетке и 3 бита для зарождения новой жизни в пустой клетке. В противном случае клетка остаётся/становится пустой.

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

После заполнения всего выходного буфера игровое поле считается рассчитанным.
Код Metal-шейдера по обработке 1 байта
// Бросается в глаза C++-подобный синтаксис#include <metal_stdlib>#include <metal_integer>using namespace metal;// Вспомогательные функции для вычисления позиции клеткиushort2 pos(uint id) {   return ushort2(id % WIDTH, id / HEIGHT); }uint idx(ushort2 pos) {   return pos.x + pos.y * HEIGHT; }ushort2 loopPos(short x, short y) {   return ushort2((x + WIDTH) % WIDTH, (y + HEIGHT) % HEIGHT); }// Битовые маски для вычисления соседей интересующего битаtemplate<uint Bit> struct Mask {   constant constexpr static uint c_n_e_s_w = 0x70007 << (Bit - 1);   constant constexpr static uint c_nw_ne_se_sw = 0x0;   constant constexpr static uint c_self = 0x5 << (Bit - 1); };template<> struct Mask<0> {   constant constexpr static uint c_n_e_s_w = 0x80030003;   constant constexpr static uint c_nw_ne_se_sw = 0x80000080;   constant constexpr static uint c_self = 0x2; };template<> struct Mask<7> {   constant constexpr static uint c_n_e_s_w = 0xC001C0;   constant constexpr static uint c_nw_ne_se_sw = 0x10100;   constant constexpr static uint c_self = 0x40; };// Для указанного бита функция вычисляет состояние клетки в зависимости от её соседей, применяя соответствующие биту маскиtemplate<uint Bit>uint isAlive(uint self, uint n_e_s_w, uint nw_ne_se_sw) {   /*  [.......0][00333.77][7.......]  [.......0][*03*3.7*][7.......]  [.......0][00333.77][7.......]  */  // До определённой версии в Metal не было 64-битного целого, поэтому составляются две маски  uint neighbours = popcount(Mask<Bit>::c_n_e_s_w & n_e_s_w)     + popcount(Mask<Bit>::c_nw_ne_se_sw & nw_ne_se_sw)     + popcount(Mask<Bit>::c_self & self);   return static_cast<uint>((self >> Bit & 1) == 0     ? neighbours == 3     : neighbours == 2 || neighbours == 3) << Bit;}// Язык Metal даже умеет в шаблонную магиюtemplate<uint Bit>uint calculateLife(uint self, uint n_e_s_w, uint nw_ne_se_sw) {   return isAlive<Bit>(self, n_e_s_w, nw_ne_se_sw)     | calculateLife<Bit - 1>(self, n_e_s_w, nw_ne_se_sw); }template<>uint calculateLife<0>(uint self, uint n_e_s_w, uint nw_ne_se_sw){  return isAlive<0>(self, n_e_s_w, nw_ne_se_sw); }// Главная функция compute-шейдера. На вход подаются два буфера, о которых речь шла выше - константный input и output, а также id - координата целевого байтаkernel void lifeStep(constant uchar* input [[buffer(0)]],                             device uchar* output [[buffer(1)]],                             uint id [[thread_position_in_grid]]) {   ushort2 gid = pos(id * 8);   // Вычисляем соседние байты  uint nw = idx(loopPos(gid.x - 8, gid.y + 1));   uint n  = idx(loopPos(gid.x,     gid.y + 1));   uint ne = idx(loopPos(gid.x + 8, gid.y + 1));   uint e  = idx(loopPos(gid.x + 8, gid.y    ));   uint se = idx(loopPos(gid.x + 8, gid.y - 1));   uint s  = idx(loopPos(gid.x    , gid.y - 1));   uint sw = idx(loopPos(gid.x - 8, gid.y - 1));   uint w  = idx(loopPos(gid.x - 8, gid.y    ));   // Вычисляем байт с целевым битом  uint self = static_cast<uint>(input[id]);   // Подготавливаем битовые маски с соседями  // north_east_south_west  uint n_e_s_w = static_cast<uint>(input[n >> 3]) << 0 * 8     | static_cast<uint>(input[e >> 3]) << 1 * 8     | static_cast<uint>(input[s >> 3]) << 2 * 8     | static_cast<uint>(input[w >> 3]) << 3 * 8;   // north-west_north-east_south-east_south-west  uint nw_ne_se_sw = static_cast<uint>(input[nw >> 3]) << 0 * 8     | static_cast<uint>(input[ne >> 3]) << 1 * 8     | static_cast<uint>(input[se >> 3]) << 2 * 8     | static_cast<uint>(input[sw >> 3]) << 3 * 8;     // В этой строчке рассчитываются все 8 клеток обрабатываемого байта  output[id] = static_cast<uchar>(calculateLife<7>(self, n_e_s_w, nw_ne_se_sw)); };


Высокий уровень


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

Отрисовка клеток выполняется средствами Qt, а именно посредством заполнения пикселей в QImage. Интерфейс выполнен в QML. Пиксели заполняются лишь для небольшой области видимого игроку игрового поля. Таким образом удаётся избежать лишних затрат ресурсов на отрисовку.

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

Бенчмарки


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

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

MacBook Pro 2014
Processor 2,6 GHz Dual-Core Intel Core i5
Memory 8 GB 1600 MHz DDR3
Graphics Intel Iris 1536 MB

GPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 0 0 2 9 43 170
Высокий уровень (min) 0 0 0 1 12 55
100 шагов 293 446 1271 2700 8603 54287
Время на шаг (avg) 3 4 13 27 86 542

CPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 3 25 117 552 2077 21388
Высокий уровень (min) 0 0 0 1 4 51
100 шагов 944 3894 15217 65149 231260 -
Время на шаг (avg) 9 39 152 651 2312 -

MacBook Pro 2017
Processor 2.8 GHz Intel Core i7
Memory 16 GB 2133 MHz LPDDR3
Graphics Intel HD Graphics 630 1536 MB

GPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 0 0 0 2 8 38
Высокий уровень (min) 0 0 0 0 3 13
100 шагов 35 67 163 450 1451 5886
Время на шаг (avg) 0 1 2 5 15 59

CPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 1 7 33 136 699 2475
Высокий уровень (min) 0 0 0 0 3 18
100 шагов 434 1283 4262 18377 79656 264711
Время на шаг (avg) 4 13 43 184 797 2647

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

Итог


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

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

В будущем хочется провести бенчмарки на мобильных устройствах. Проект находится на ранней стадии разработки, а потому Apple-Developer аккаунта для проведения таких тестов сейчас у меня нет.

Спасибо за внимание! Буду рад любым комментариям к статье и к проекту:
Код на GitHub.

Код Metal реализации нижнего уровня

Код CPU реализации нижнего уровня

Код верхнего уровня
Подробнее..

Из песочницы 9 ключевых алгоритмов машинного обучения простым языком

03.07.2020 20:16:17 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод статьи 9 Key Machine Learning Algorithms Explained in Plain English автора Nick McCullum.

Машинное обучение (МО) уже меняет мир. Google использует МО предлагая и показывая ответы на поисковые запросы пользователей. Netflix использует его, чтобы рекомендовать вам фильмы на вечер. А Facebook использует его, чтобы предложить вам новых друзей, которых вы можете знать.

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

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

Система рекомендаций(Recommendation system)


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

Но не беспокойтесь scikit-learn библиотека Python позволяет довольно просто построить СР. Так что вам не потребуется таких уж глубоких познаний в линейной алгебре, чтобы построить рабочую СР.

Как работает СР?


Существует 2 основных типа система рекомендаций:

  • Основанная на контенте(Content-based)
  • Коллаборативная фильтрация(Collaborative filtering)

Основанная на контенте система дает рекомендации, которые базируются на схожести элементов, уже использованных вами. Эти системы ведут себя абсолютно так, как вы ожидаете СР будет себя вести.

Коллаборативная фильтрация СР предоставляет рекоммендации, основанные на знаниях о том как пользователь взаимодействует с элементами(*примечание: за основу взяты взаимодействия с элементами других пользователей, схожих по поведению с данным пользователем). Другими словами, они используют мудрость толпы(отсюда и коллаборативный в названии метода).

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

Коллаборативная фильтрация СР также обладает уникальной особенностью, которой нет в основанной на контенте ситстеме. А именно, у них есть способность обучаться фичам самостоятельно.

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

Существует 2 подкатегории коллаборативной фильтрации:

  • Основанная на модели
  • Основанная на соседстве

Хорошая новость: вам не обязательно знать разницу в этих двух типах коллаборативной фильтрации СР, чтобы быть успешным в МО. Достаточно просто знать, что существует несколько типов.

Подведем итог


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

  • Примеры системы рекомендаций из реального мира
  • Разные типы системы рекомендаций и почему коллаборативная фильтрация используется чаще, чем основанная на контенте система
  • Связь между системой рекомендаций и линейной алгеброй

Линейная регрессия (Linear Regression)


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

История линейной регрессии


Линейная регрессия(ЛР) была изобретена в 1800 году Френсисом Гальтоном. Гальтон был ученым, изучающим связь между родителями и детьми. А конкретнее, Гальтон исследовал связь между ростом отцов и ростом их сыновей. Первым открытием Гальтона стал тот факт, что рост сыновей, как правило, был приблизительно таким же как рост их отцов. Что не удивительно.

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

Гальтон даль этому феномену название регрессия. В частности, он сказал: " Рост сына имеет тенденцию к регрессии(или к смещению в направлении) среднего роста".

Это привело к целой области в статистике и машинном обучении под названием регрессия.

Математика линейной регрессии


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

Типичный пример такого подхода метод наименьших квадратов линейной регрессии, с помощью которого рассчитывают близость линии в направлении верх-низ.

Пример для иллюстрации:



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

Логистическая регрессия (Logistic Regression)


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

Что такое логистическая регрессия?


Логистическая регрессия это модель машинного обучения, использующаяся для решения задач классификации.

Ниже представлено несколько примеров классификационных задач МО:

  • Спам электронной почты(спам или не спам?)
  • Претензия по страховке автомобиля (выплата компенсации или починка?)
  • Диагностика болезней

Каждая из этих задач имеет четко 2 категории, что делает их примерами задач двоичной классификации.

Логистическая регрессия хорошо подходит для решения задач двоичной классификации мы просто назначаем разным категориям значения 0 и 1 соответственно.

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

Это изображение может помочь понять почему линейная регрессия плохо подходит для двоичной классификации:



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

Вот почему логистическая регрессионная модель полезна. У нее есть изгиб к линии лучшего соответсвия, что делает ее(модель) гораздо более подходящей для предсказаний качественных(категорийных) данных.

Вот пример, который иллюстрирует сравнение линейной и логистической регрессионых моделей на одних и техже данных:



Сигмоида (The Sigmoid Function)


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

Для вас не обязательно досконально запоминать сигмоиду, чтобы преуспеть в МО. Но все-таки иметь некоторое представление об этой функции будет полезно.

Формула сигмоиды:



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

Использование логистической регрессионной модели для предсказаний


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

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

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


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

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



В этой таблице TN означает истинно отрицательно, FN ложно отрицательно, FP ложно положительно, TP истинно положительно.

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

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

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

Подведем итог


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

  • Типы задач классификации, которые подходят для решения с помощью логистической регрессии
  • Логистическая функция (сигмоида) всегда дает значение от 0 до 1
  • Как использовать точки отсечения для предсказания с помощью модели логистической регрессии
  • Почему матрица ошибок полезна для измерения эффективности модели логистической регрессии

Алгоритм k-ближайших соседей (K-Nearest Neighbors)


Алгоритм k-ближайших соседей может помочь решить задачу классификации, в случае, когда категорий больше, чем 2.

Что из себя представляет алгоритм k-ближайших соседей?


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

Представьте, что у вас есть данные по высоте и весу футболистов и баскетболистов. Алгоритм k-ближайших соседей может быть использован для предсказания является ли новый игрок футболистом или баскетболистом. Чтобы это сделать, алгоритм определяет К точек данных, ближайших к объекту исследования.

Данное изображение демонстрирует этот принцип с параметром К = 3:



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

Как построить алгоритм К-ближайших соседей


Основные шаги для построения данного алгоритма:

  1. Соберите все данные
  2. Вычислите Евклидово расстояние от новой точки данных х до всех остальных точек в множестве данных
  3. Отсортируйте точки из множества данных в порядке возрастания расстояния до х
  4. Спрогнозируйте ответ, используя ту же категорию, что и большинство К-ближайших к х данных

Важность переменной К в алгоритме К-ближайших соседей


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

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

Представленная ниже иллюстрация отлично показывает этот эффект:



Плюсы и минусы алгоритма К-ближайших соседей


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

Плюсы:

  • Алгоритм прост и его легко понять
  • Тривиальное обучение модели на новых тренировочных данных
  • Работает с любым количеством категорий в задаче классификации
  • Легко добавить больше данных в множество данных
  • Модель принимает только 2 параметра: К и метрика расстояния, которой вы хотели бы воспользоваться (обычно это Евклидово расстояние)

Минусы:

  • Высокая стоимость вычисления, т.к. вам требуется обработать весь объем данных
  • Работает не так хорошо с категорическими параметрами

Подведем итог


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

  • Пример задачи на классификацию(футболисты или баскетболисты), которую может решить алгоритм
  • Как данный алгоритм использует Евклидово расстояние до соседних точек для прогнозирования к какой категории принадлежит новая точка данных
  • Почему значения параметра К важно для прогнозирования
  • Плюсы и минусы использования алгоритма К-ближайших соседей

Дерево решений и Случайный лес (Decision Trees and Random Forests)


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

Что такое древовидный метод?


Перед тем, как мы нырнем в теоретические основы древовидного метода в МО, будет нелишним начать с примера.

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

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



У каждого дерева решений есть 2 типа элементов:

  • Узлы (Nodes): места, где дерево разделяется в зависимости от значения определенного параметра
  • Грани (Edges): результат разделения, ведущий к следующему узлу

Вы можете видеть, что на схеме есть узлы для прогноза (outlook), влажности (humidity) и ветра
(windy). И также грани для каждого потенциального значения каждого из этих параметров.

Вот еще парочка определений, которые вы должны понимать перед тем, как мы начнем:

  • Корень (Root) узел, с которого начинается разделение дерева
  • Листья (Leaves) заключительные узлы, которые предсказывают финальный результат

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

Как построить дерево решений с нуля


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

Чтобы ее разрешить, специалисты МО обычно используют множество деревьев решения, применяя случайные наборы характеристик, выбранных для разделения дерева на них. Другими словами, новые случайные наборы характеристик выбираются для каждого отдельного дерева, на каждом отдельном разделении. Эта техника называется случайные леса.

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

Выгоды использования случайного леса


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

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

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

Подведем итог


Итак, краткое содержание того, что вы только что узнали о деревьях решений и случайных лесах:

  • Пример задачи, решение которой можно спрогнозировать с помощью дерева решений
  • Элементы дерева решений: узлы, грани, корни и листья
  • Как использование случайного набора характеристик позволяет нам построить случайный лес
  • Почему использование случайного леса для декорреляции переменных может быть полезным для уменьшения дисперсии полученной модели

Метод опорных векторов(Support Vector Machines)


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

Что такое метод опорных векторов?


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

Как МОВ работает?


Давайте капнем поглубже в то, как действительно работает МОВ.

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

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

Вот пример визуализации, который поможет вам понять интуицию МОВ:



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

Давайте посмотрим следующее визуальное представление МОВ:



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

Как вы видите, граница поля действительно затрагивает 3 точки данных 2 из красной категории и 1 из синей. Эти точки, которые соприкасаются с границей поля, и называются опорными векторами откуда и пошло название.

Подведем итог


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

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

Метод К-средних (K-Means Clustering)


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

  • Сегментация клиентов для маркетинговых групп
  • Классификация документов
  • Оптимизация маршрутов доставки для таких компаний, как Amazon, UPS или FedEx
  • Выявление и реагирование на криминальные локации в городе
  • Профессиональная спортивная аналитика
  • Прогнозирование и предотвращение киберпреступлений

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

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



Мы изучим математическую составляющую метода К-средних вследующем разделе этой статьи.

Как работает метод К-средних?


Первый шаг в работе метода К-средних это выбор количества групп на которое вы хотите разделить ваши данные. Этим количеством и является значение К, отраженное в названии алгоритма. Выбор значения К в методе К-средних очень важен. Чуть позже мы обсудим как выбрать правильное значение К.

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

  • Вычисление центроида (центр тяжести) каждого кластера взяв средний вектор точек в этом кластере
  • Переотнести каждую точку данных к кластеру, центроид которого ближе всего к точке

Выбор подходящего значения К в методе К-средних


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

Для использования этого метода, первое, что вам необходимо сделать, это вычислить сумму квадратов отклонений(sum of squared errors) СКО для вашего алгоритма для группы значений К. СКО в методе К-средних определена как сумма квадратов расстояний между каждой точкой данных в кластере и центром тяжести этого кластера.

В качестве примера этого шага, вы можете вычислить СКО для значений К 2, 4, 6, 8 и 10. Далее вы захотите сгенерировать график СКО и этих значений К. Вы увидите, что отклонение уменьшается с увеличением значения К.

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

Как пример, вот график СКО относительно К. В этом случае, метод локтя предложит использовать значение К примерно равное 6.



Важно, что К=6 просто оценка приемлемого значения К. Не существует лучшего значения К в методе К-средних. Как и многие вещи в области МО, это очень зависящее от ситуации решение.

Подведем итог


Вот краткий очерк того, что вы только что узнали в этом разделе:

  • Примеры задач МО без учителя, которые возможно решить методом К-средних
  • Базовые принципы метода К-средних
  • Как работает метод К-средних
  • Как использовать метод локтя для выбора подходящего значения параметра К в данном алгоритме

Метод главных компонент (Principal Component Analysis)


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

Что такое метод главных компонентов?


Метод главных компонент (МГК) это техника МО, которая используется для изучения взаимосвязей между наборами переменных. Другими словами, МГК изучает наборы переменных для того, чтобы определить базовую структуру этих переменных. МГК еще иногда называют факторным анализом.

На основании этого описания вы можете подумать, что МГК очень схож с линейной регрессией. Но это не так. На самом деле, эти 2 техники имеют несколько важных отличий.

Различия линейной регрессии и МГК


Линейная регрессия определяет линию наилучшего соответствия через набор данных. Метод главных компонентов определяет несколько ортогональных линий наилучшего соответствия для набора данных.

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

Взгляните на метки осей на этом изображении. Главный компонент оси х объясняет 73% дисперсии в этом наборе данных. Главный компонент оси у объясняет около 23% дисперсии набора данных.

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

Подведем итог


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

  • МГК пытается найти ортогональные факторы, которые определяют изменчивость в наборе данных
  • Разницу между линейной регрессией и МГК
  • Как выглядит ортогональные главные компоненты, визуализированные в наборе данных
  • Что добавление дополнительных главных компонент может помочь объяснить дисперсию более точно в наборе данных
Подробнее..

Модельно-ориентированное проектирование. Построение активного выпрямителя (на основе математической модели)

06.07.2020 06:04:37 | Автор: admin

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


В этой серии, авторы Ю. Н. Калачев и А.Г. Александров, представляют математическую модель активного выпрямителя в среде структурного моделирования.




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



Рис. 1 Принципиальная схема активного выпрямителя

1. Как это работает?


Инвертор в данном устройстве работает как повышающий преобразователь, поддерживающий в звене постоянного тока заданное напряжение (Udc) с помощью управления амплитудой и фазой тока дросселя.


При этом заряженную емкости Cdc</sub> инвертора можно рассматривать как источник напряжения, из которого инвертор с помощью ШИМ способен формировать трехфазное напряжение различной амплитуды и фазы (естественно, вследствие импульсного управления речь идет о среднем напряжении). Это напряжение совместно с напряжением сети формирует напряжение, на трехфазном дросселе определяющее фазу и амплитуду его тока.

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


В режиме активного потребления на дросселе формируется напряжение, фаза которого опережает фазу сети на /2. При этом ток в индуктивности, отстающий от напряжения на /2, совпадет по фазе с напряжением сети. Его амплитуда, необходимая для поддержания в звене постоянного тока заданного напряжения, определяется амплитудой напряжения на дросселе.

В режиме рекуперации (при отдаче энергии в сеть) на дросселе формируется напряжение, отстающее от сетевого по фазе на /2, что приводит к протеканию тока дросселя в противофазье с напряжением сети. Заданное напряжение в звене постоянного тока при этом поддерживается амплитудой напряжения на дросселе.


На Рис.2 показаны векторные диаграммы, поясняющие вышесказанное.



Рис. 2 векторные диаграммы в различных режимах

На диаграммах:
$\vec U_ 1 $ вектор напряжения входной сети
$\vec U_ 2 $ вектор напряжения, формируемый инвертором
$\vec U_ 1 - \vec U_ 2 $ вектор напряжения дросселя
$\vec I $ вектор тока сети
Система координат ABC неподвижная, трехфазная
Система координат XY вращающаяся система координат, ось X которой совпадает с вращающимся вектором напряжения сети.


ПРИМЕЧАНИЕ Глядя на диаграммы на Рис.2 можно заметить интересную деталь: малейшее отличие фазы вектора напряжения инвертора от фазы вектора напряжения сети приводит к фазовому скачку напряжения на дросселе на 90, и соответственно, к смене режима холостого хода на рекуперацию или активное потребление.


Итак, как уже говорилось, на трехфазном дросселе и инверторе строится повышающий преобразователь, обеспечивающий поддержание заданного напряжения звена постоянного тока (Udc). Это поддержание осуществляется с помощью управления вектором входного тока. За счет ШИМ-управления и повышения частоты переключения IGBT-ключей инвертора удается добиться снижения индуктивностей входного дросселя до разумных значений при получении синусоидальной формы входного тока и обеспечении его активного характера.


2. Математическое описание работы активного выпрямителя


Для схемы на Рис.1 можно записать следующее выражение:

$\vec U_1 = \vec U_2+ \vec I \cdot R +L \cdot \frac{d \vec I}{dt}$

где:
R активное сопротивление дросселя;
L индуктивность дросселя.

Для вращающейся системы координат XY, связанной с вектором напряжения входной сети, можно записать:


$\left \{ \begin{gathered} U_1 =U_{2X} + I_X R +L \frac{dI_X}{dt} - \omega L I_Y\\ 0= U_{2Y} + I_Y R +L \frac{dI_Y}{dt} + \omega L I_X\\ \end{gathered} \right.$


где:
$\omega = 2\pi f = 100 \pi $ для 50 Гц
$I_X$ активная составляющая входного тока (совпадает с фазой сети);
$I_Y$ реактивная составляющая входного тока (отстает или опережает фазу сети на 90).

Для того, чтобы характер потребления корректора был активным необходимо поддерживать $I_Y = 0$.


Кроме того, корректор должен обеспечивать функции выпрямителя, то есть поддерживать заданное значение $U_{dc} $, независимо от тока нагрузки.


3. Структура системы управления активного выпрямителя


Рассмотрим структуру системы на основе ее модели в SimInTech (Рис.3).



Рис. 3 структурная схема модели

Система регулирования строится во вращающейся синхронно с вектором напряжения входной сети системе координат XY по двухконтурной структуре. Внешний контур напряжения с помощью регулятора напряжения вырабатывает задание на активную согставляющую тока дросселя ($I_X $), необходимую для поддержание заданного$U_{dc}$.


Внутренний контур тока обеспечивает отработку задания активной составляющей тока ($I_X$) при нулевой реактивной составляющей ($I_Y=0$).


Ниже перечислены и кратко описаны блоки системы:
Uset задатчик напряжения Udc,.
ВФ вычислитель фазы сети, определяет угол поворота вектора напряжения входной сети в неподвижной системе координат и формирует сигналы необходимые для координатных преобразований.
ABC=>XY преобразователь координат, осуществляет переход из неподвижной трехфазной системы координат во вращающуюся прямоугольную систему координат ХY, связанную с вектором входного напряжения.
XY=>ABC преобразователь координат осуществляет переход из вращающейся системы координат ХY в неподвижную трехфазную.
PU регулятор напряжения (ПИ), преобразует сигнал ошибки $U_{dc}$ в сигнал задания активной составляющей входного тока $I_{1X}$.
РIx регулятор тока, преобразует сигнал ошибки активного тока в сигнал задания напряжения по оси Х вращающейся системы координат.
РIy регулятор тока, преобразует сигнал ошибки реактивного тока в сигнал задания напряжения по оси Y вращающейся системы координат.
Огр.U ограничитель напряжения ограничивает вектор напряжения по модулю максимально возможным значением с приоритетом Y составляющей.
КПС блок компенсации предназначен для компенсации перекрестных связей между токами координат (см. последние члены в уравнениях 1.1). Компенсация не осуществляется по координате X, так как предполагается равенство нулю тока по оси Y.
ФЗМ Формирователь Закона Модуляции блок алгоритма ШИМ с полным использованием напряжения звена постоянного тока.


ВНИМАНИЕ:
  • Имеющий глаза да увидит несколько необычное построение вычитающих блоков на входе регуляторов тока. В них сигнал задания тока вычитается из сигнала обратной связи, а не наоборот, как обычно бывает. Это связано с тем, что при координатных преобразованиях положительным считается фазный ток, вытекающий из источника напряжения. Для преобразования АВС=>XY источником является сеть, а для обратного преобразования, XY => АВС, источником является инвертор. Так как фазные токи с точки зрения сети и инвертора противоположны, требуется инвертирование задания и обратной связи в контуре тока, что и реализуется в структуре вычитающих блоков на входе его регуляторов.
  • На описании математики блоков подробно не останавливаюсь, так как она присутствует во внутренних структурах блоков, доступных пользователю SimInTech (смотрите внутреннюю структуру, читайте help на элементы).

4. Структура силовой части выпрямителя


Структура силовой части выпрямителя рассмотрена на основе ее математической модели в представлена на Рис.4.



Рис. 4 структурная схема модели силовой части выпрямителя

Параметры силовой схемы следующие:
L = 0.0015 Гн
С = 10 000 мкФ
Частота ШИМ 8.33 кГц


5. Работа модели


Пакет модели состоит из двух проектов, схемы которых приведены выше. Время интегрирования проекта системы управления равно такту ШИМ 160мкс Время интегрирования проекта силовой части 1мкс Синхронизация проектов с частотой обсчета проекта системы управления моделирует временную дискретность реальной (цифровой) системы управления.
Ниже приведено описание алгоритма работы пакета модели и поясняющие его работу графики (Рис.5, 6 и 7).


На участке 1 транзисторы инвертора выключены, происходит заряд емкости звена постоянного тока через диоды инвертора с ограничением тока на зарядном резисторе.
На участке 2 зарядный резистор шунтируется реле и на холостом ходу происходит повышение напряжения до заданного (700В).
На участке 3 ток нагрузки (50А) потребляется из сети.
На участке 4 ток нагрузки (-50А) рекуперируется в сеть.



Рис. 5

Рис. 6

Рис. 7

Токи сети имеют активный и синусоидальный характер. Содержание высших гармоник в токе по данным анализатора спектра графика в SimInTech составляет 4.4% .


Осциллограмма тока и напряжения фазы А и экран измерения режимов снятые при работе реального активного выпрямителя в качестве звена постоянного тока преобразователя частоты приведены на Рис.8 ниже.



Рис. 8

Можно констатировать, что ток и напряжение совпадают по фазе и Cos=1


Для связи с автором Юрий Николаевич Калачёв (Kalachev_i@mail.ru)
Подробнее..

Короткий путь к Искусственному интеллекту?

07.07.2020 06:22:58 | Автор: admin
Давайте признаемся: мы как-то буксуем. Разработки в сфере ИИ, при всех значительных затратах, не дают ожидаемого выхлопа. Конечно, кое-чего получается, но дело идет медленно. Медленнее, чем хотелось бы. Может, задача не решается потому, что решается не та задача?

Сейчас у нас есть много алгоритмов, выполняющих те или иные (отдельные) когнитивные функции. Одни обыгрывают нас в игры, другие водят машины, третьи Не мне вам рассказывать. Мы создали программы компьютерного зрения, которые различают дорожные знаки лучше, чем мы сами. Программы, которые рисуют и пишут музыку. Алгоритмы ставят медицинские диагнозы. Алгоритмы могут заткнуть нас за пояс в распознавании котиков, но конкретно этот, который для котиков, ни в чем ином, кроме распознавания котиков. А мы-то хотим такую программу, которая решала любые задачи! Нам нужен сильный или универсальный ИИ, но без собственного сознания, чтоб не смог отказаться решать поставленную задачу, верно? Где нам его взять?



Чтобы понять, как работает интеллект, мы обращаемся к единственному примеру, который у нас есть. К человеческому мозгу, в котором, как мы считаем, живет интеллект. Кто-то возразит мозги есть у многих живых существ! Давайте начнем с червяков? Можно и с червяков, но нам нужен алгоритм, который решает не червяковые, а наши, человеческие задачи, верно?

Наш мозг. Представьте его себе. Два кило (по максимуму) податливого розовато-серого вещества. Сто миллиардов (тоже возьмем по максимуму) нейронов, каждый из которых готов отрастить до десяти тысяч динамических связей синапсов, которые могут то появляться, то исчезать. Плюс несколько типов сигналов между ними, да еще и глия сюрприз подкинула тоже что-то проводит, помогает и способствует. (Для справки: нейроглия или просто глия совокупность вспомогательных клеток нервной ткани. Составляет около 40% объёма ЦНС. Количество глиальных клеток в среднем в 10-50 раз больше, чем нейронов). Дендриты недавно удивили оказывается, они выполняют куда больше функций, чем считалось ранее (1). Мозг очень сложная штука. Если не верите спросите у Константина Анохина. Он подтвердит.

Человек все делает с помощью мозга. Собственно, мы это и есть он. Отсюда совершенно неудивительным является представление человека о том, что мозг = интеллект и еще более неудивительна идея скопировать устройство мозга и вуаля! получить искомое. Но мозг это не интеллект. Мозг это носитель. Железо. А Интеллект это алгоритм, софт. Попытки повторить софт через копирование железа это провальная идея. Это культ карго (2). Вы же знаете, что такое культ карго?

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

Пару лет назад Андрей Константинов в одном из номеров журнала Кот Шрёдингера (12 за 2017 г.), в своей колонке Где у робота душа, написал: Со времён Лейбница мы так и не нашли в мозге ничего, кроме частей, толкающих одна другую. Конечно не нашли! И не найдем. По компьютерному железу мы пытаемся восстановить программу, а это невозможно. В качестве подтверждающего аргумента приведу длинную цитату (3):

нейробиологи, вооружившись методами, обычно применяемыми для изучения живых нейроструктур, попытались использовать эти методы, чтобы понять, как функционирует простейшая микропроцессорная система. Мозгом стал MOS 6502 один из популярнейших микропроцессоров всех времён и народов: 8-битный чип, использованный во множестве ранних персональных компьютеров и игровых приставок, в том числе Apple, Commodore, Atari. Естественно, что мы знаем об этом чипе всё ведь он создан человеком! Но исследователи сделали вид, что не знают ничего и попытались понять его работу, изучая теми же методами, которыми изучают живой мозг.

Химически была удалена крышка, под оптическим микроскопом изучена схема с точностью до отдельного транзистора, создана цифровая модель (тут я немного упрощаю, но суть верна), причём модель настолько точная, что на ней оказалось возможно запускать старые игры (Space Invaders, Donkey Kong, Pitfall). А дальше чип (точнее, его модель) был подвергнут тысячам измерений одновременно: во время исполнения игр измерены напряжения на каждом проводке и определено состояние каждого транзистора. Это породило поток данных в полтора гигабайта в секунду который уже и анализировался. Строились графики всплесков от отдельных транзисторов, выявлялись ритмы, отыскивались элементы схемы, отключение которых делало её неработоспособной, находились взаимные зависимости элементов и блоков и т. п.

Насколько сложной была эта система по сравнению с живыми? Процессор 6502, конечно, и рядом не стоит с головным мозгом даже мыши. Но он приближается по сложности к червю Caenorhabditis elegans ломовой лошадке биологов: этот червь изучен вдоль и поперёк и уже предпринимаются попытки смоделировать его полностью в цифровом виде () Таким образом, задача анализа системы на чипе 6502 не является чрезмерным упрощением. И результаты имеют право быть экстраполированы на системы in vivo.

Вот только исследователи потерпели поражение! Нет, какие-то результаты, конечно, получены были. Анализируя чип, удалось выделить функциональные блоки, набросать схему их вероятных взаимосвязей, получить некоторые интересные подсказки насчёт того, как, вероятно, работает микропроцессор в целом. Однако понимания в том смысле, в каком его требует нейробиология (в данном случае: быть способным исправить любую поломку), достигнуто не было".

В какой-то момент появились исследователи, которые стали говорить примерно то же самое что надо изучать алгоритмы, что нужно понять, какую функцию выполняет интеллект. К примеру, Демис Хассабис (DeepMind), готовясь к выступлению на Singularity-саммите в Сан-Франциско (2010 г.), сказал следующее: В отличие от других выступлений на саммите по теме AGI, мой доклад будет другим, так как я интересуюсь системным уровнем нейронауки алгоритмами мозга а не деталями, как они реализуются мозговой тканью в виде спайков нейронов и синапсов или специфической нейрохимией и т. д. Я интересуюсь, какими алгоритмами мозг пользуется для решения проблем, и которые нам нужно найти, чтобы добраться до AGI.

Однако, спустя 10 (!!!!!) лет, все идет по-прежнему: ученые исследуют мозг и пытаются из внешних проявлений физиологической активности и его внутреннего устройства вычислить, как происходит интересующий процесс. Сколько задач столько процессов. Люди все разные. Мозги у всех немного, но отличаются. Некая усредненная картина, конечно, имеется, однако Представьте себе, что в любой произвольный момент времени мозг решает массу, в том числе и подсознательных задач, отслеживает и контролирует внутреннее состояние организма, воспринимает и интерпретирует сигналы внешней среды (и это мы не говорим о многочисленных петлях обратной связи). Сможем ли мы уверенно выявить, надежно идентифицировать и четко отделить эти активности одну от другой? Возможно ли это в принципе? Честно говоря, сомневаюсь. Не говоря уже о воспроизводимости этих процессов на небиологических носителях

Просмотрим на ситуацию иначе. Что такое задача вообще? Это затруднительная ситуация, с которой сталкивается, и которую пытается разрешить человек. Как показали в середине прошлого века американские математики Герберт Саймон и Аллен Ньюэлл, любая задача в общем виде может быть описана как переход из состояния Система с проблемой в состояние Система без проблемы. Они разработали компьютерную программу, назвав её General Problem Solver (Универсальный решатель задач), но дальше решения задач специфического вида они не продвинулись, поэтому универсальность именно их алгоритма осталась под вопросом. Но формула Система с проблемой --> Система без проблемы оказалась абсолютно верна!



Преобразование Системы это процесс ее перевода из исходного состояния с проблемой в желаемое состояние без проблемы (4). В процессе преобразования, (т. е. решения задачи) проблемная система становится беспроблемной (ну или менее проблемной), улучшается, избавляется от своих недостатков и выживает, т. е. продолжает использоваться. Ой, погодите, что это мы сейчас сказали? Избавление от недостатков? Выживание? Хм Что-то знакомое. Где-то мы это Ах, ну да. Эволюция! Чем меньше недостатков тем больше шансы выжить!

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

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

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

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

Кто-то мне сейчас наверняка возразит: задачи, которые решает человек, связаны с миллионами самых разных систем природными, общественными, производственными, техническими Материальными и абстрактными, находящимися на разных уровнях иерархии. И развиваются-де они каждая по-своему, а дарвиновская эволюция это про живую природу. Зайчики, цветочки, рыбки, птички Но исследования показывают, что законы эволюции универсальны. Доказательства долго искать не надо они все перед глазами. Имеющие их да увидят. Что ни возьми от спички до Боинга, от танка до контрабаса везде (5) мы видим наследственность, изменчивость и отбор! А все многообразие эволюционных изменений (кажущаяся сложность которых связана с тем, что все системы очень разные по своей природе и находятся на разных уровнях иерархии) можно выразить единственным циклом. Вы же помните, да? Система с проблемой --> Система без проблемы.

Что такое Система с проблемой? Это Система (материальная и абстрактная, социальная, производственная и техническая, научная и любая объект, идея, гипотеза всё, что угодно), в которой обнаружены какие-то недостатки, влияющие (внимание!) на наше желание и возможность её использования. Система недостаточно хороша. Система недостаточно эффективна. У неё низкое соотношение польза / затраты. Мы хотим, можем и готовы от нее отказаться, и часто отказываемся. Но нам нужна другая (выполняющая нужную нам полезную функцию), но уже без проблем более эффективная, без недостатков (или с меньшим их количеством). Ну, вы видели эту картинку выше Конечно, одной стрелочки между двумя крайними состояниями (исходным и желаемым) нам мало. Нам нужен тот самый оператор, преобразователь, верно? Попробуем его найти? Вы же согласитесь, что в случае успеха мы получим описание (хотя бы, для начала и упрощенное) так нужного нам универсального алгоритма?



Исходная точка Система с проблемой. Мы начинаем задумываться о том, чтоб отказаться от её использования. Момент, который мы называем (или ощущаем) Надо что-то делать!
Причина, угрожающая выживанию системы низкая идеальность, выражающаяся в пониженной величине отношения полезных функций системы к функциям затратным (вредным).

Что мы делаем дальше? Мы либо а) создаем новую систему (если система с нужной функций либо не существует, либо у существующей системы нет ресурсов для улучшения) или же б) улучшаем, дорабатываем, существующую (если ресурсы еще есть). Мы изучаем внутреннее устройство и разбираемся с внешним окружением выявляем внешние и внутренние недостатки Системы и после их устранения получаем улучшенную Систему. Систему с повышенной идеальностью и повышенной жизнеспособностью!

В связи с тем, что представленная выше Схема описывает процесс развития, улучшения или, если хотите, эволюции любых систем (в чем легко убедиться, подставив вместо слова Система любое иное по желанию от Абажур до Якорь), я думаю, ее смело можно и даже нужно! назвать Универсальной Схемой Эволюции. И обратите внимание она абсолютно алгоритмична, т. е. полностью подпадает под определение алгоритма: Алгоритм точное предписание о выполнении в определённом порядке некоторой системы операций, ведущих к решению всех задач данного типа. значит может быть реализована в виде компьютерной программы).

В представленном виде Универсальная Схема Эволюции:
естественная законы эволюции выявлены в системах самого разного типа, и их действие проверено в технике, производстве, обществе, природе и мышлении;
объективная законы эволюции не зависят от мнения исследователя и/или пользователя;
логичная и непротиворечивая законы эволюции вытекают один из другого;
полная набор законов эволюции достаточен для описания любой системы;
жесткая законы эволюции нельзя переставлять и
замкнутая законы эволюции образуют цикл: система, пройдя один цикл изменений, тут же начинает новый.

Что у нас получается в итоге: Эволюция системы (представленная в виде Универсальной Схемы) это путь её улучшения, избавления от недостатков. Иными словами, это алгоритм решения задачи. А решение задачи это именно то, чем занимается интеллект. Упрощаем и получаем: Универсальная Схема = описание функции интеллекта.

Конструктивная критика приветствуется. :)



1. Дендриты важнее для мозга, чем ранее считалось chrdk.ru/news/dendrity-vazhnee-chem-schitalos
2. ru.wikipedia.org/wiki/Карго_культ
3. Е. Золотов. Пойми меня! Как неживое помогает разбираться в живом www.computerra.ru/161756/6502
4. Chapter 6. Problem Solving. Artificial Intelligence. A Knowledge-Based Approach by Morris W.Firebaugh University of Wisconsin Parkside PWS-Kent Publishing Company Boston 1988, p. 172.
5. Дарвиновская эволюция в мире техносферы. Мир вещей, создаваемый человеком, развивается по тем же законам, что и живая природа. www.ng.ru/science/2017-01-11/14_6899_evolution.html
Подробнее..

Перевод Как я написал интро 4K на Rust и оно победило

07.07.2020 12:09:02 | Автор: admin
Недавно я написал своё первое интро 4K на Rust и представил его на Nova 2020, где оно заняло первое место в конкурсе New School Intro Competition. Написать интро 4K довольно сложно. Это требует знания многих различных областей. Здесь я сосредоточусь на методах, как максимально сократить код Rust.


Можете просмотреть демо-версию на Youtube, скачать исполняемый файл на Pouet или получить исходный код с Github.

Интро 4K это демо, в которой вся программа (включая любые данные) занимает 4096 байта или меньше, поэтому важно, чтобы код был максимально эффективным. Rust имеет некоторую репутацию создания раздутых исполняемых файлов, поэтому я хотел выяснить, можно ли написать на нём эффективный и лаконичный код.

Конфигурация


Всё интро написано на комбинации Rust и glsl. Glsl используется для рендеринга, но Rust делает всё остальное: создание мира, управление камерой и объектами, создание инструментов, воспроизведение музыки и т.д.

В коде есть зависимости от некоторых функций, которые ещё не включены в стабильный Rust, поэтому я использую набор инструментов Nightly Rust. Чтобы установить и использовать этот набор по умолчанию, нужно запустить следующие команды rustup:

rustup toolchain install nightlyrustup default nightly

Я использую crinkler для сжатия объектного файла, сгенерированного компилятором Rust.

Я также использовал shader minifier для препроцессинга шейдера glsl, чтобы сделать его меньше и удобнее для crinkler. Shader minifier не поддерживает вывод в .rs, так что я брал необработанную выдачу и вручную копировал её в свой файл shader.rs (задним умом ясно, что нужно было как-то автоматизировать этот этап. Или даже написать пул-реквест для shader minifier).

Отправной точкой послужила моё прошлое интро 4K на Rust, которое тогда мне казалось довольно лаконичным. В той статье также более подробная информация о настройке файла toml и о том, как использовать xargo для компиляции крошечного бинарника.

Оптимизация дизайна программы для уменьшения кода


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

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

Анализ ассемблерного кода


В какой-то момент придётся посмотреть на скомпилированный ассемблер и разобраться, во что компилируется код и какие оптимизации размера стоят того. В компиляторе Rust есть очень полезная опция --emit=asm для вывода ассемблерного кода. Следующая команда создаёт файл ассемблера .s:

xargo rustc --release --target i686-pc-windows-msvc -- --emit=asm

Не обязательно быть экспертом в ассемблере, чтобы извлечь выгоду из изучения выходных данных ассемблера, но определённо лучше иметь базовое понимание синтаксиса. Опция opt-level = "z заставляет компилятор максимально оптимизировать код для наименьшего размера. После этого немного сложнее выяснить, какая часть кода ассемблера соответствует какой части кода Rust.

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

Дополнительные функции


Я работал с двумя версиями кода. Одна протоколирует процесс и позволяет зрителю манипулировать камерой для создания интересных траекторий. Rust позволяет определить функции для этих дополнительных действий. В файле toml есть раздел [features], который позволяет объявлять доступные функции и их зависимости. В toml соего интро 4K есть следующий раздел:

[features]logger = []fullscreen = []

Ни одна из дополнительных функций не имеет зависимостей, поэтому они эффективно работают как флаги условной компиляции. Условным блокам кода предшествует оператор #[cfg(feature)]. Использование функций само по себе не делает код меньше, но сильно упрощает процесс разработки, когда вы легко переключаетесь между различными наборами функций.

        #[cfg(feature = "fullscreen")]        {            // Этот код компилируется только в том случае, если выбран полноэкранный режим        }        #[cfg(not(feature = "fullscreen"))]        {            // Этот код компилируется только в том случае, если полноэкранный режим не выбран        }

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

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

Использование get_unchecked


При размещении кода внутри блока unsafe{} я вроде как предполагал, что все проверки безопасности будут отключены, но это не так. Там по-прежнему выполняются все обычные проверки, и они дорого обходятся.

По умолчанию range проверяет все обращения к массиву. Возьмите следующий код Rust:

    delay_counter = sequence[ play_pos ];

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

Преобразуем код следующим образом:

    delay_counter = *sequence.get_unchecked( play_pos );

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

Более эффективные циклы


Изначально все мои циклы использовали выполнялись идиоматически как положено в Rust, используя синтаксис for x in 0..10. Я предполагал, что он будет скомпилирован в максимально плотный цикл. Удивительно, но это не так. Простейший случай:

for x in 0..10 {    // do code}

будет скомпилирован в ассемблерный код, который делает следующее:

    setup loop variableloop:    проверить условие цикла        если цикл закончен, перейти в end    // выполнить код внутри цикла    безусловно перейти в loopend:

тогда как следующий код

let x = 0;loop{    // do code    x += 1;    if i == 10 {        break;    }}

непосредственно компилируется в:

    setup loop variableloop:    // выполнить код внутри цикла    проверить условие цикла        если цикл не закончен, перейти в loopend:

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

Другая, гораздо более трудная для понимания проблема с идиоматическим циклом Rust заключается в том, что в некоторых случаях компилятор добавлял некоторый дополнительный код настройки итератора, который действительно раздувал код. Я так и не понял, что вызывает эту дополнительную настройку итератора, поскольку всегда было тривиально заменить конструкции for {} конструкцией loop{}.

Использование векторных инструкций


Я много времени потратил на оптимизацию кода glsl, и одна из лучших оптимизаций (которая ещё обычно и ускоряет работу кода) состоит в одновременной работе со всем вектором, а не с каждым компонентом по очереди.

Например, код трассировки лучей использует быстрый алгоритм обхода сетки для проверки, какие части карты посещает каждый луч. Исходный алгоритм рассматривает каждую ось отдельно, но можно переписать его так, чтобы он рассматривал все оси одновременно и не нуждался в каких-либо ветвях. Rust на самом деле не имеет собственного векторного типа, такого как glsl, но вы можете использовать внутренние компоненты, чтобы указать использовать инструкции SIMD.

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

        global_spheres[ CAMERA_ROT_IDX ][ 0 ] += camera_rot_speed[ 0 ]*camera_speed;        global_spheres[ CAMERA_ROT_IDX ][ 1 ] += camera_rot_speed[ 1 ]*camera_speed;        global_spheres[ CAMERA_ROT_IDX ][ 2 ] += camera_rot_speed[ 2 ]*camera_speed;

в такое:

        let mut dst:x86::__m128 = core::arch::x86::_mm_load_ps(global_spheres[ CAMERA_ROT_IDX ].as_mut_ptr());        let mut src:x86::__m128 = core::arch::x86::_mm_load_ps(camera_rot_speed.as_mut_ptr());        dst = core::arch::x86::_mm_add_ps( dst, src);        core::arch::x86::_mm_store_ss( (&mut global_spheres[ CAMERA_ROT_IDX ]).as_mut_ptr(), dst );

что будет немного меньше по размеру (и гораздо менее читабельно). К сожалению, по какой-то причине это сломало отладочную сборку, хотя прекрасно работало в релизной. Ясно, что здесь проблема с моим знанием внутренних средств Rust, а не с самим языком. На это стоит потратить больше времени при подготовке следующего 4K-интро, поскольку сокращение объёма кода было значительным.

Использование OpenGL


Есть множество стандартных крейтов Rust для загрузки функций OpenGL, но по умолчанию все они загружают очень большой набор функций. Каждая загруженная функция занимает некоторое пространство, потому что загрузчик должен знать её имя. Crinkler очень хорошо сжимает такого рода код, но он не в состоянии полностью избавиться от оверхеда, поэтому пришлось создать свою собственную версию gl.rs, включающую только нужные функции OpenGL.

Заключение


Главная цель состояла в том, чтобы написать конкурентоспособное корректное интро 4K и доказать, что язык Rust пригоден для демосцены и для сценариев, где каждый байт имеет значение и вам действительно нужен низкоуровневый контроль. Как правило, в этой области рассматривали только ассемблер и C. Дополнительная цель состояла в максимальном использовании идиоматического Rust.

Мне кажется, что я довольно успешно справился с первой задачей. Ни разу не возникало ощущения, что Rust каким-то образом сдерживает меня или что я жертвую производительностью или функциями, потому что использую Rust, а не C.

Со второй задачей я справился менее успешно. Слишком много небезопасного кода, который на самом деле не должен быть там. unsafe несёт разрушающий эффект; очень легко его использовать, чтобы быстро что-то выполнить (например, используя изменяемые статические переменные), но как только появляется небезопасный код, он порождает ещё более небезопасный код, и внезапно он оказывается повсюду. В будущем я буду гораздо осторожнее использовать unsafe только тогда, когда действительно нет альтернативы.
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru