ВходНаше всё Теги codebook 无线电组件 Поиск Опросы Закон Четверг
2 мая
560437 Топик полностью
Николай Коровин (18.11.2014 01:21, просмотров: 78) ответил Скрипач на А давай! 8)
Пост четвёртый из стапиццот. Семейка булыжников и шейдеры-не-по-назначению. Если бы лет 30 назад кто-нибудь сказал бы про меня, что я буду выбирать между тем, чтобы писать под камень или писать для целого семейства камней, вероятно, он причинил бы моей семье немало горя, так как все дружно решили бы, что из меня вырастет писатель-шизофреник. Судя по этим микростатьям, так и вышло ээээ я хотел сказать, что при всей лиричности данного отступления вопрос весьма интересный. Писать под конкретный камень, конечно, здорово и результат крайне шустр, но на все камни не напасёшься вариантов кода. Писать же под семейство камней, то есть под младший из совместимых сверху -- автоматически похерить все плюсы более новых. Также в гаджетах ощутимо гадит то обстоятельство, что камни могут иметь самую неожиданную конфигурацию портов и памяти; конечно, всё это можно одним махом завернуть в виртуальную машину, работающую по принципу пре-компиляции абстрактного кода ВМ в конкретный код камня "по месту" (интерпретирующие ВМ даже не рассматриваются из-за полного слива по скорости), но даже ВМ с пре-компиляцией, как показывает практика, "оптимизируют" в результате такой двойной трансляции конечный нативный код так, что хочется залезть в машину времени и вернуться во времена 8080-х, когда ТАКОГО не было. Вдобавок многие вещи, такие, как endianness, требуют внимательности именно в написании изначального кода и никакая ВМ не сможет из little-кода сделать код для big-камня, если программист не рассматривал такой вариант изначально. Проще бы уже было вообще отказаться от непрозрачности своего кода и распространять C-исходники, которые собирались бы по месту, даже будь они в результате видны всему белому свету и стократно украдены в итоге; с кодом, например, на Java, как его ни обфусцируй, всё равно постоянно происходит то же самое, не лучше ли сразу сдаться? Увы, даже такая жертва не даёт оптимальной сборки на любом первом попавшемся камне. Да и работоспособности без кросс-платформенных тестов, строго говоря, тоже не даёт в общем случае. Практика показывает, что тонкости и нюансы безжалостно перевешивают. Даже абсурдные решения (я видел проверку endianness в драйвере 3dFX для Linux, каковой 3dFX, естественно, в принципе может стоять только на x86 PC) не позволяют предусмотреть всё. Отсюда ещё одно важное замечание: не надо огромной ценой закладывать в систему возможность достичь "суперменской цели", которой 99% разработчиков не достигнут всё равно, потому что они не супермены. Цена будет уплачена, а почти недостижимая цель достигнута не будет. "Таки шо я с этого буду иметь, Изя? Таки ничего, одну "потенциальную возможность"? Я таки пrедпочитаю матеrиальные, а не потенциальные!" К счастью, разделение котлет и мух может выручить и здесь. Вместо виртуальной машины можно ограничиться "виртуальным мотором". Компилятор разделяет код на три части: "под младший камень", "уточнить по месту" и "вызвать системную функцию". В первую часть код идёт по умолчанию. Если что-то, грубо выражаясь, реализовано через циклы for -- видимо, это какая-то та логика программы, которая не полагается на массовую обработку данных и вообще не критична к скорости. Легче всего это себе представить на x86: по умолчанию код идёт под Pentium I без MMX. На i7 он точно запустится в своём натуральном бинарном виде, как, впрочем, и на Pentium I. Вторая часть... вторую часть мы рассмотрим позже. Это очень мощный механизм, но попытки применять его не по назначению его безудержно парализуют. Третья часть же весьма эффективна, но ещё более ограничена по назначению: сделать что-то, выходящее за пределы стандарта, нельзя вообще. Тем не менее, эта ветвь прекрасно развивается, начиная как минимум с DirectDraw, если не раньше. Всякая уважающая себя система давно предоставляет набор API для 2D-масштабирования изображений, косинусных преобразований, анализа Фурье и прочих библиотечных матопераций, которые (драйверами или непосредственной сборкой системы под целевую конфигурацию) оптимизированы именно под имеющиеся на данном устройстве процессоры, сопроцессоры и акселераторы. Тем не менее, крайне не рекомендуется делать какой-либо набор мандатным. Программа должна уметь сама обслужить себя, если, скажем, адрес вызова функции косинусного преобразования система вернула как NULL. Не имея специальных средств ускорения, системная функция проиграет "велосипеду", даже написанному первым типом кода, но зато строго под конкретные константы, размеры и параметры, нужные именно нам, так что в таком случае и "обещать не должна". И, в особо экзотических случаях, программисту не возбраняется снабдить программу целым набором предельно оптимизированных "велосипедов" под популярные варианты железа, игнорируя системные библиотеки при любой возможности: "спортивно-оптимизационное" программирование такой подход не запрещает, в отличие от ВМ, но и не принуждает к нему, в отличие от бросания один на один с целым семейством злых камней. Нетрудно видеть, что вторая часть, собирающая код "по месту", и третья, предоставляющая набор функций -- суть компилирующая и интерпретирующая недо-ВМ, "разобранная до мотора", причём помещённого в условия, идеальные для того или другого подхода. И проще сделать две элементарные вещи, чем одну сложную -- "экономия должна быть экономной", не забыли? Продуманная архитектура открывает простор для элементарных вещей, которых мы сейчас лишены просто по принципу имбецильной девочки из анекдота: "А что, можно было, дааа?" Если человек много работает на планшете с графикой (я не говорю про существующие ныне планшеты, предназначенные исключительно для потреблядства говноконтента, потому что лучше, жёстче и правильнее, чем Лурк, я всё равно сказать про них не смогу; я говорю про продуманную архитектуру и софт, какими они могли и должны бы были, по идее, быть), обрабатывает изображения, сшивает видео -- достаточно приобрести только что вышедший планшет со встроенным DSP-сопроцессором, и имеющийся у него привычный софт немедленно, без ожидания портирования, прибавит скорости втрое за счёт скинутых на бедный DSP функций косинусного преобразования, вейвлетов и прочих радостей жизни. Программистам и так хватает задач выше крыши, кроме переписывания из пустого в порожнее. В конце концов, мы только что перешли наконец к написанию прикладных программ, использующих восстановление потерянной резкости изображения через дифференцирование по заданной форме отклика от точечного источника! Задача с известной математикой и огромной потребительской ценностью, ждавшая своих покорителей лет 15, если не 20. Какая безработица, полноте. Быдлокодеры? О, от безработицы ЭТИХ "программистов" только выиграет сельское хозяйство. Непьющие плотники сейчас в очень большом дефиците. Вернёмся же наконец ко второму пункту: компиляция части кода "по обстоятельствам". С недостатками интерпретирующей ВМ мы разобрались, поручив ей делать только то, что она делает во много раз быстрее, чем произвольно-универсальный код самой программы, причём столь шустрая ВМ всё это время скрывалась у нас под носом в чёрной зорровской маске API. Как же мы справимся с упорным нежеланием прекомпилирующей ВМ выдавать нам код, идеально оптимизированный для данной платформы? Как всегда, 90% тормозов сосредоточены в 10% рутинного кода, перепахивающего большие объёмы. Этот код имеет если не простую, то по крайней мере однообразную структуру, и это даёт нам в руки сильное средство: мы можем обречь бедных программистов писать этот код не на нормальных языках "среднего уровня" типа Си и уж тем более не на языках высокого уровня, а на некоем мозговзрывном "квазиассемблере", вся структура которого подчинена не облегчению труда программиста, как яваподобные языки со сборщиками мусора и уютненькими классами, а предельно развитым возможностям задать избыточную информацию о том, чего ты от машины хочешь. Мне крайне сложно наглядно и доходчиво изложить идеологию и особенно примеры такого языка, но суть его в том, чтобы сложно описывать простые вещи с тем, чтобы они могли быть как можно оптимальнее применены к случайной платформе. Например, простое умножение двух 32-битных чисел в 64 с последующим взятием 32-битного остатка от деления на 32-битное на Си придётся выражать через __int64, что вынудит компилятор на 32-битной платформе произвести кучу лишних действий; на x86 ассемблере же это просто умножение dw на dw, результат которого хранится в регистровой паре из двух dw, которая тут же может быть использована в качестве делимого и после деления на dw даст частное и остаток в регистровой паре, оба будут простыми dw. Боюсь, я тут вступаю на тонкий лёд абсолютной непроработанности этого вопроса кем-либо, потому что "задротский язык", предназначенный для полировки до блеска некоей малой части кода, заслуживающей такой чести, явно противоречит всем модным трендам и едва ли кто-то брался за такую задачу. Одно только декларируется приятно и легко: это распараллеливание. Возможность явно указать, что порядок исполнения неких моментов может быть произвольным, позволяет системе, знакомой, скажем, с MMX, сложить какие-то регистры друг с другом "чохом", а не поштучно, позволит раскидать по ядрам работу с разными частями массива, позволит... да-да, мы подошли к очень важному моменту. for (i=1024;i<j;i++) a[i]^=mask; //компилятор не знает, чем ограничена область массива и не может без хитрого анализа догадаться, что порядок обработки не важен for each i(1024 to j where j<1024+256) a[i]^=mask; //компилятор знает, что область массива в любом случае не больше 256, и порядок индексов не важен А теперь представьте себе, что у нас есть какая-то матрица из трёх сотен примитивных вычислителей, что-то такое шейдерообразное. Ну, или даже ПЛИС, на которой мы ради этой программы можем сконфигурировать 300 тупых инверторов. И представьте себе, какие возможности нам открывает априорное знание того, что j минус начало не превысит 256. Или представьте себе переборный коррелометрический алгоритм, в котором вычисляется сумма (по всем ячейкам 2D-массива) модулей разниц между ячейкой и неким её соседом, сначала с левым, потом с верхним, потом через одного... представьте, что даёт возможность задекларировать это вычисление так, чтобы ячейки вычитались и модули разности суммировались, допустим, в 1024 потока. Представили? Вот это и катапультирует нас в упомянутую мельком в заголовке экологическую нишу OpenCL. Но, перед тем, как вспомнить OpenCL, перечитайте ещё раз мою формулировку постановки вопроса, каким должен быть язык для "узких мест", и наложите это на OpenCL. Это даст вам хорошую пищу для ума и шанс на озарение, на которое у меня кишка тонка. А вот про OpenCL и будет, наверное, следующий пост. Но не про язык, про который я не берусь судить, а опять про неграмотные архитектуры. Тем более что тред всё-таки архитектурный, а не языковой.