Entry tags:
динамическая загрузка Dex файла в Dalvik
В общем и с явовской виртуальной машиной вышло по сценарию -- "хотели как лучше, а получилось -- как всегда." ;-) Вроде бы делали универсальный байткод, который write once, run anywhere... Сделали. Но теперь неймется компании Google, которая мало того, что написала для Явы новую виртуальную машину, да еще к тому-же и совершенно другой архитектуры.
Большинству, конечно, все равно, они свои программы перекомпилируют специальным гуглевским компилятором (который из обычных Java class files делает Dalvik-овские dex файлы). А что делать нам, несчастным писателям компиляторов ? ;-)
Но мало того, то байткод другой архитектуры. Еще и загружать динамически его приходится по-новому. И не просто по-новому, оказывается сам этот байткод Dalvik загрузить непосредственно не может, только из zip архива... В общем, грабли на граблях.
Ну вот я вроде разобрался. Пишу, чтобы зафиксировать. Вам это, скорее всего, будет не интересно. ;-)
Итак, возьмем программу Hello.java с одним единственным методом hello, который очевидно что делает. Задача в том, чтобы загрузить этот файл в Андроид динамически, непосредственно в двоичном виде.
Тоесть загрузить нужно Вот этот zip файл, созданный командой:
Ну не жуть ли ? ;-)
Так что, наступает эра многоплатформенных компиляторов в явовские байткоды со всеми вытекающими "прелестями". Да, явовский байткод не очень оптимален в плане используемого пространства, но неужели такая оптимизация, всего-лишь в несколько раз, стоила того ? И все равно-ж, гады, в процессе загрузки этого .zip с файлом .dex (вызов конструктора DexFile), они докомпилируют его в "оптимизированный" байткод в формате .odex, который еще хранят в отдельном файле.
Вот такая вот бессмысленная политика (ибо по-другому мне это сложно объяснить). Нет, для меня это, конечно, выгодно. Я то думал проект уже закончен. А тут опа, появляется возможность переписать все заново (с сохранением старого, проверенного и удобного API). Типа раз, и снова в девках. ;-) Хорошо, но бессмысленно.
По-хорошему нужно было спрятать свой dalvik подальше (как implementation detail, раз уж они верят, что именно такой подход даст им performance edge), включить в стандартную поставку перекомпилятор dx в прозрачном режиме и пройти явовский testsuite.
Большинству, конечно, все равно, они свои программы перекомпилируют специальным гуглевским компилятором (который из обычных Java class files делает Dalvik-овские dex файлы). А что делать нам, несчастным писателям компиляторов ? ;-)
Но мало того, то байткод другой архитектуры. Еще и загружать динамически его приходится по-новому. И не просто по-новому, оказывается сам этот байткод Dalvik загрузить непосредственно не может, только из zip архива... В общем, грабли на граблях.
Ну вот я вроде разобрался. Пишу, чтобы зафиксировать. Вам это, скорее всего, будет не интересно. ;-)
Итак, возьмем программу Hello.java с одним единственным методом hello, который очевидно что делает. Задача в том, чтобы загрузить этот файл в Андроид динамически, непосредственно в двоичном виде.
Тоесть загрузить нужно Вот этот zip файл, созданный командой:
javac Hello.java && \ dx --dex --output=Hello.zip Hello.class && \ adb -e push Hello.zip /data/localЧтобы это сделать, нужна вот такая программа SayHello.java, которая компилируется и работает вот так:
javac SayHello.java && \ dx --dex --output=SayHello.zip SayHello.class && \ adb -e push SayHello.zip /data/local \ adb -e shell dalvikvm -classpath /data/local/SayHello.zip SayHello Note: SayHello.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 43 KB/s (2034 bytes in 0.045s) Hi ! Im' your "Hello World" loader ! First, I'm hooking the DexFile class...Good. Second, I'm getting hold of its constructor...Good. Third, I'm instantiating the DexFile object on "/data/local/Hello.zip"...Good. Fourth, I'm getting to its loadClass method...Good. Fifth, loading Hello class from the loaded dex...Good. Sixth, getting hello method of Hello class...Good. Seventh, invoking the hello method: Hello WORLD ! Done. Easy, isn't it ? ;-)
Ну не жуть ли ? ;-)
Так что, наступает эра многоплатформенных компиляторов в явовские байткоды со всеми вытекающими "прелестями". Да, явовский байткод не очень оптимален в плане используемого пространства, но неужели такая оптимизация, всего-лишь в несколько раз, стоила того ? И все равно-ж, гады, в процессе загрузки этого .zip с файлом .dex (вызов конструктора DexFile), они докомпилируют его в "оптимизированный" байткод в формате .odex, который еще хранят в отдельном файле.
Вот такая вот бессмысленная политика (ибо по-другому мне это сложно объяснить). Нет, для меня это, конечно, выгодно. Я то думал проект уже закончен. А тут опа, появляется возможность переписать все заново (с сохранением старого, проверенного и удобного API). Типа раз, и снова в девках. ;-) Хорошо, но бессмысленно.
По-хорошему нужно было спрятать свой dalvik подальше (как implementation detail, раз уж они верят, что именно такой подход даст им performance edge), включить в стандартную поставку перекомпилятор dx в прозрачном режиме и пройти явовский testsuite.
no subject
То, что вначале делали преимущественно стековые процессоры (но и те, не чисто теоретические, а с возможностью залазить в глубь стека и работать с его верхом, почти как с регистрами), а сейчас делают преимущественно регистровые (но содержащие инструкции для работы со стеком) может быть данью моде...
Теоретически не очевидно -- что лучше. Ни в железе. Ни, тем более в VM.
Преимущества и недостатки одной архитектуры можно покрыть преимуществами и недостатками другой. Но, если у Вас есть мнение и конкретные аргументы -- говорите. Я буду оппонировать.
К.Л.М.
no subject
Ну, инструкции для работы со стеком есть потому что стек используется, конечно - просто не для вычислений, а для spilling-а локальных переменных и для передачи параметров/хранения return value, зависит от ABI.
Все нижесказанное - домыслы. Я неплохо умею писать код под регистровые архитектуры, и никогда не задумывался про чисто стековую модель (с адресацией в середину, понятно) в железе. Возможно, я ошибаюсь во всем нижеизложенном :)
Насколько я понимаю жизнь, стековая архитектура немного усложняет вычисление выражений с общими частями (т.е. появляются лишние инструкции добавления на вершину стека элемента из его середины) и очень препятствует вычислению выражений в случае, если инструкции имеют латентность больше такта. В регистровых машинах зависимости идут по регистрам, в простых стековых машинах зависимости, видимо, бесполезны, потому что любая инструкция зависит от stack pointer и его меняет :) Получается, что в стековой машине придется про верхние N элементов стека помнить, когда станет доступно значение в этом элементе - т.е. фактически сделать из верхушки стека набор регистров. По идее, если сделать следующий шаг и вынести распил между регистрами и стеком на софтварный уровень - железо упростится, и появятся возможности для доп. оптимизаций (переиспользование регистров).
В процессорах уже достаточно давно любят делать dual-issue. С трудом понимаю, как он может работать на одном стеке.
Накладные расходы на декодирование инструкций, по идее, должны быть меньше, и вообще конвеер должен быть проще в регистровых - в трехадресной архитектуре, чтобы сложить две локальных переменных, нужна одна инструкция, в стековой - 3 (да, каждая проще).
Непонятно, что делать с разными наборами вычислительных юнитов - делать три стека? Вряд ли, они должны быть неограниченного размера - иначе смысл в стеке? :) - тогда непонятно, как менеджить память. Делать один стек, на который можно класть что угодно? Если типы разных размеров (int32, float64, vector128), то видимо очень неудобно получается, когда нужно оптимизировать. Плюс, то же самое что и выше про dual-issue - обычно латентности разных юнитов можно скрывать работой других юнитов, кажется, что такое очень сложно делать на одном стеке.
no subject
Как я себе представляю, многих проблем, для которых придуманы красивые методы оптимизации, в стековой архитектуре просто бы не было.
Ведь регистры -- это просто близкая к ALU, специализированная кэш-память. В стековой архитектуре они не нужны вообще, вместо них была-бы просто кэш-память, не специализированная, которая хранила бы наиболее часто адресуемые адреса из основной памяти, содержащей номинально безграничный стек. Это позволило-бы делать процессоры более масштабируемыми, сделало бы оптимизацию более динамической (что хранить в кэше на этапе выполнения программы виднее, чем на этапе статического ее анализа). Не нужно было-бы перекомпилировать программу под разные под-типы процессоров, с разной шириной и количеством регистров, а просто процессор с большим кэшем, автоматически использовал бы его полностью и содержал бы по-сути большее количество регистров совершенно прозрачно для программиста и компилятора.
Стековая архитектура (с кэшем вместо регистров и неограниченным стеком) лучше в многозадачной среде, поскольку при переключении задач не нужно было бы сохранять содержимое всех регистров, задачи совершенно прозрачно использовали бы по-максимуму общий кэш.
Никаких проблем нет и с суперскалярной архитектурой. В стековой машине можно применить по-сути целиком и полностью register renaming, только переименовывать не регистры, а несколько верхних элементов стека (сколько -- опять-же было-бы всего-лишь несущественной деталью реализации), что позволило бы развязать кажущиеся зависимости по данным и выполнять несколько инструкций параллельно.
В стеке, конечно, нужно хранить не какие-то конкретные типы данных, а машинные слова. Типы данных определяются поверх машинных слов, это более высокоуровневая концепция.
Многие вещи, которые в регистровой архитектуре можно оптимизировать, как, например, распределение регистров (особенно если они немного различаются функционально, как в x86, что делает проблему нетривиальной), в стековой оптимизировать не нужно, поскольку нет регистров -- нет проблем.
Поскольку инструкции простые и "свободы" в них мало, львиная доля оптимизации была бы в процессоре (а не в компиляторе), который имеет возможность подстроиться под конкретный свой обьем кэша и времена выполнения каждой инстркции. Это, повторюсь, позволило бы сделать процессоры более масштабируемыми. Мыслимо было бы сделать целую линейку процессоров (от встраиваемых и до высокопроизводительность), которые выполняли бы одни и те-же команды без перекомпиляции (и без черной магии в виде instruction scheduling) с максимальной эффективностью на каждом из них. Причем, транзисторов для этого потребовалось бы меньше (а значит их можно было бы использовать для дополнительного параллелизма, лучшего управления кэшем-регистрами и т.д.).
К.Л.М.
no subject
управления кэшем-регистрами --> управления кэш памятью, играющей роль регистров
no subject
А, и еще надо уметь все кеши которые есть на верхушку стека (состояния "регистров" т.е. - в стиле "значение есть на int конвеере, можно забирать") уметь сбрасывать по любой записи на стек другой инструкцией этого потока, ну или при загрузке cache line.
Я всегда считал, что главное достоинство стековой модели - простота концепции, ну и тот факт что древовидные вычисления естественным образом представимы в виде набора стековых инструкций. Вроде бы, как только надо серьезно оптимизировать, все становится сложнее, чем в регистровой.
no subject
Мне кажется, это должно быть просто и эффективно. По крайней мере, в сравнении с монстрозной архитектурой x86/x87 -- гораздо проще и гораздо эффективнее.
К.Л.М.
no subject
no subject
Так будет плохо потому, что при прямом доступе к оперативной памяти есть только операции считывания и записи, но нет операции стирания (запись невозможно отменить). Тоесть, если некоторая формула использует более одного промежуточного значения при вычислении результата, ячейки, соответствующие этим промежуточным значениям, будут "dirty" и их таки прийдется по окончании расчета записать из кэша в память**. А шина работает (и всегда будет работать по фундаментальным причинам) гораздо медленнее процессора.
Стек неявно подразумевает операцию стирания. При правильной реализации кэша в описанной мной стековой машине промежуточные значения (вставленные в стек, а затем убранные из него в процессе расчета) в оперативную память никогда записаны не будут.
К.Л.М.
*При небольших ухищрениях (добавив несколько инструкций), на самом деле, можно сделать и так. Но это будет некрасиво.
** Этой проблемы не возникает, если в процессе счета операции производятся только над одной ячейкой памяти, которая и будет по окончании расчета содержать результат. Но это слишком частный случай, когда формула -- лишь последовательность унарных операций.