Оптимизация оценки дерева логической логики

У меня есть много истинных/ложных результатов, сохраненных в виде битов в массивах long[]. У меня их огромное количество (миллионы и миллионы лонгов).

Например, скажем, у меня есть только пять результатов, я бы получил:

+----- condition 5 is true
|
|+---- condition 4 is false
||
||+--- condition 3 is true
|||
|||+-- condition 2 is true
||||
||||+- condition 1 is false
10110

У меня также есть несколько деревьев, представляющих такие утверждения, как:

condition1 AND (condition2 OR (condition3 AND condition 4))

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

class Node {    
    int operator();
    List<Node> nodes;
    int conditionNumber();    
}

По сути, либо узел является листом, а затем имеет номер условия (соответствующий одному из битов в массивах long[]), либо узел не является листом и, следовательно, ссылается на несколько подузлов.

Они просты, но позволяют выражать сложные логические выражения. Это прекрасно работает.

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

Поэтому мне нужно пройтись по дереву и ответить либо true, либо false в зависимости от содержимого дерева и содержимого long[].

Метод, который мне нужно оптимизировать, выглядит так:

boolean solve( Node node, long[] trueorfalse ) {
   ...
}

где при первом вызове node является корневым узлом, а затем, очевидно, подузлами (будучи рекурсивным, этот метод solve вызывает сам себя).

Зная, что у меня будет всего несколько деревьев (может быть, до сотни или около того), но миллионы и миллионы long[] нужно проверить, какие шаги я могу предпринять, чтобы оптимизировать это?

Очевидное рекурсивное решение передает параметры ((sub)tree и long[], я мог бы избавиться от long[], не передавая его в качестве параметра) и довольно медленно со всеми рекурсивными вызовами и т. д. Мне нужно проверить, какой оператор используется (И, или ИЛИ, или НЕ и т. д.), и задействовано довольно много операторов if/else или switch.

Я не ищу другого алгоритма (их нет), поэтому я не ищу перехода от O (x) к O (y), где y будет меньше x.

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

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

Любая идея, кроме создания байт-кода для каждого дерева?

Теперь, если я хочу попробовать маршрут генерации байт-кода, как мне это сделать?


person SyntaxT3rr0r    schedule 11.04.2011    source источник
comment
Для любопытных, моя реализация Node мало чем отличается от этой: stackoverflow.com/questions/1830925   -  person SyntaxT3rr0r    schedule 11.04.2011
comment
Одна очевидная вещь - это короткое замыкание как можно скорее (например, ЛОЖЬ И все, что ЛОЖНО, и ИСТИНА ИЛИ все, что ИСТИННО). Я полагаю, вы уже делаете это?   -  person Jeff Foster    schedule 11.04.2011
comment
@Джефф Фостер: да, да, я делаю это :) Но могут быть относительно большие деревья, состоящие из большого количества операторов ИЛИ и всего нескольких операторов И, поэтому даже с использованием короткого замыкания я все равно хотел бы это улучшить. :)   -  person SyntaxT3rr0r    schedule 11.04.2011
comment
Если вы считаете, что предварительная компиляция дерева в байт-код жизнеспособна, просто выполните полную длину и сгенерируйте машинные инструкции (на C, используя JNI). Домен bool only достаточно прост в сборке, чтобы этот компромисс между риском и выгодой работал, ИМХО.   -  person sehe    schedule 11.04.2011
comment
Вы оцениваете одно и то же дерево с более чем одним набором значений?   -  person phkahler    schedule 11.04.2011


Ответы (3)


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

Возможно, вы захотите профилировать его, подсчитывая

  • какие ветви И оцениваются как ложные
  • какие ветви ИЛИ приводят к истинному

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

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

Я надеюсь, что все это имеет смысл, потому что я понимаю, что прозаическая версия скучна. Однако, как сказал Ферма, пример кода слишком велик, чтобы уместиться в это поле :)

person sehe    schedule 11.04.2011
comment
классный ответ, но... Оба массива long[] и деревья создаются динамически. Будет ли предложенное вами профилирование работать в этом случае? Например, могу ли я связать это с генерацией байт-кода? - person SyntaxT3rr0r; 11.04.2011
comment
Динамика вещей затрудняет какие-либо прогнозы в целом. Вы можете получить лучшую стоимость/выгоду, просто оптимизировав постоянные затраты на производительность (т.е. скомпилировать в байт-код/сборку как есть) - person sehe; 11.04.2011

Существует простой и быстрый способ оценить логические операции, подобные этой, в C. Предполагая, что вы хотите оценить z=(x op y), вы можете сделать это:

 z = result[op+x+(y<<1)];

Таким образом, op будет кратным 4, чтобы выбрать вашу операцию AND, OR, XOR и т. д. Вы создаете таблицу поиска для всех возможных ответов. Если эта таблица достаточно мала, вы можете закодировать ее в одно значение и использовать сдвиг вправо и маску для выбора выходного бита:

z = (MAGIC_NUMBER >> (op+x+(y<<1))) & 1;

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

Для маленьких деревьев это выигрыш. Для более крупных деревьев с коротким замыканием это, вероятно, не является выигрышем, потому что среднее количество ветвей, которые необходимо оценить, составляет от 2 до 1,5, что является огромным выигрышем для больших деревьев. YMMV.

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

person phkahler    schedule 11.04.2011
comment
такие вещи заставляют меня очень скучать по моей сборке и C-дням. К сожалению, на данный момент я застрял с Java :( но +1 за определенно хороший ответ! - person SyntaxT3rr0r; 11.04.2011
comment
@SyntaxT3rr0r: Спасибо. Я использовал это однажды для моделирования цифровой логики. Смотрите редактирование для короткого замыкания со списком ;-) - person phkahler; 11.04.2011

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

((word&1) && ((word&2) || ((word&4) && (word&8))))

Это может быть скомпилировано на лету всякий раз, когда деревья меняются, и загружается результирующий байт-код / ​​dll, и все это занимает меньше секунды.

Дело в том, что в настоящее время вы интерпретируете содержимое деревьев. Превращение их в скомпилированный код должно заставить их работать в 10-100 раз быстрее.

ДОБАВЛЕНО в ответ на ваши комментарии об отсутствии JDK. Затем, если вы не можете сгенерировать байт-код Java, я бы попытался написать свой собственный интерпретатор байт-кода, который работал бы как можно быстрее. Это может выглядеть примерно так:

while(iop < nop){
  switch(code[iop++]){
    case BIT1: // check the 1 bit and stack a boolean
      stack[nstack++] = ((word & 1) != 0);
      break;
    case BIT2: // check the 2 bit and stack a boolean
      stack[nstack++] = ((word & 2) != 0);
      break;
    case BIT4: // check the 4 bit and stack a boolean
      stack[nstack++] = ((word & 4) != 0);
      break;
    // etc. etc.
    case AND: // pop 2 booleans and push their AND
      nstack--;
      stack[nstack-1] = (stack[nstack-1] && stack[nstack]);
      break;
    case OR: // pop 2 booleans and push their OR
      nstack--;
      stack[nstack-1] = (stack[nstack-1] || stack[nstack]);
      break;
  }
}

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

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

person Mike Dunlavey    schedule 11.04.2011
comment
ок, спасибо :) Ну ладно, тогда попробую изучить библиотеку байт-кода ASM для Java и посмотреть, что даст :) - person SyntaxT3rr0r; 11.04.2011
comment
@ SyntaxT3rr0r: можно, но я бы не стал. Я бы сгенерировал исходный код и позволил бы компилятору сгенерировать низкоуровневый материал. Это достаточно быстро. - person Mike Dunlavey; 11.04.2011
comment
к сожалению, у меня может не быть полного контроля над JDK, если он установлен на машинах, на которых он будет работать :( - person SyntaxT3rr0r; 11.04.2011
comment
@SyntaxT3rr0r: Хорошо, это усложняет задачу. Байт-код Java будет вашим следующим лучшим выбором. Если вы не можете этого сделать, вы можете сделать свой собственный интерпретатор, как я описал выше. - person Mike Dunlavey; 12.04.2011
comment
Мне нравится ваше редактирование ... Но я проверил, и библиотека ASM для Java кажется всего 50 КБ. Я не знаю, что лучше атм. Я рассмотрю оба варианта :) Но я думаю, что из-за возможности я сначала начну с того, что вы предлагаете в своем редактировании :) - person SyntaxT3rr0r; 12.04.2011