Повышение производительности симулятора ТМ

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

Задача состоит в том, чтобы смоделировать около 650 000 ТМ, каждая из которых имеет около 200 непустых входных данных. Максимальное количество шагов, которые я пытаюсь выполнить, составляет 1 миллиард (10 ** 9).

Ниже приведен код, который я запускаю. vector<vector<int> > TM — это таблица переходов.

vector<int> fast_simulate(vector<vector<int> > TM, string TM_input, int steps) {
    /* Return the state reached after supplied steps */

    vector<int> tape = itotape(TM_input);

    int head = 0;
    int current_state = 0;
    int halt_state = 2;

    for(int i = 0; i < steps; i++){

        // Read from tape
        if(head >= tape.size()) {
            tape.push_back(2);
        }
        int cell = tape[head];
        int data = TM[current_state][cell];  // get transition for this state/input

        int move = data % 2;
        int write = (data % 10) % 3;
        current_state = data / 10;

        if(current_state == halt_state) {
            // This highlights the last place that is written to in the tape
            tape[head] = 4;
            vector<int> res = shorten_tape(tape);
            res.push_back(i+1);
            return res;
        }

        // Write to tape
        tape[head] = write;

        // move head
        if(move == 0) {
            if(head != 0) {
                head--;
            }
        } else {
            head++;
        }
    }

    vector<int> res {-1};
    return res;
}

vector<int> itotape(string TM_input) {
    vector<int> tape;
    for(char &c : TM_input) {
        tape.push_back(c - '0');
    }
    return tape;
}

vector<int> shorten_tape(vector<int> tape) {
    /*  Shorten the tape by removing unnecessary 2's (blanks) from the end of it.
    */
    int i = tape.size()-1;
    for(; i >= 0; --i) {
        if(tape[i] != 2) {
            tape.resize(i+1);
            return tape;
        }
    }
    return tape;
}

Можно ли где-нибудь улучшить производительность или использование памяти? Даже снижение на 2% будет иметь заметную разницу.


person spyr03    schedule 28.03.2017    source источник
comment
Вы передаете некоторые потенциально огромные vector по значению, таким образом, активно копируя их.   -  person ForceBru    schedule 28.03.2017
comment
Кроме того, вы постоянно push_back работаете в itotape, тем самым изменяя размер tape нагрузок раз в секунду, что довольно дорого. Обратите внимание, что TM_input — это строка, размер которой вам известен, чтобы вы могли сразу выделить достаточно памяти.   -  person ForceBru    schedule 28.03.2017
comment
этот вопрос лучше подходит для codereview.stackexchange.com (при условии, что это рабочий код). Я не уверен, что он классифицируется как слишком широкий или основанный на мнении, но наверняка мало шансов, что у кого-то еще возникнет такой же вопрос.   -  person 463035818_is_not_a_number    schedule 28.03.2017
comment
@ForceBru хорошая мысль, я могу передать ленту по ссылке. Я не знаю окончательный размер ленты, поэтому есть ли смысл спрашивать длину строки памяти, если лента будет расти сразу после слов?   -  person spyr03    schedule 28.03.2017
comment
@ spyr03, TM_input - это std::string, и он уже «знает» свой размер. Я пытаюсь сказать, что вы уже знаете этот размер в itotape, поэтому вы можете сразу выделить достаточно памяти для tape, а затем заполнить его данными. В настоящее время вы постоянно увеличиваете размер tape, последовательно выделяя память, что тратит много времени.   -  person ForceBru    schedule 28.03.2017
comment
Каково среднее количество шагов за один запуск моделирования?   -  person stgatilov    schedule 28.03.2017
comment
@stgatilov у моделирования есть два общих результата: остановка менее чем за 100 шагов и выполнение полного 1 миллиарда шагов. Я не знаю, что встречается чаще, но полный 1 миллиард шагов съедает большую часть времени.   -  person spyr03    schedule 28.03.2017


Ответы (3)


Убедитесь, что в течение всей симуляции ТМ не происходит никаких распределений.

Предварительно выделяйте один глобальный массив при запуске программы, который достаточно велик для любого состояния ленты (например, элементов 10^8). Сначала поместите машину в начало этого ленточного массива. Сохраните сегмент [0; R] всех ячеек, которые были посещены текущей симуляцией машины: это позволяет избежать очистки всего массива лент при запуске новой симуляции.

Используйте наименьший целочисленный тип для элементов ленты, которого достаточно (например, используйте unsigned char, если в алфавите наверняка меньше 256 символов). Возможно, вы даже можете переключиться на наборы битов, если алфавит очень маленький. Это уменьшает объем памяти и повышает производительность кэша/ОЗУ.

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

person stgatilov    schedule 28.03.2017
comment
Почему обнуление массива после каждой симуляции происходит не медленнее, чем выделение памяти? Изменение примитива в массиве кажется действительно хорошей идеей для экономии памяти :) Что я должен гуглить, чтобы найти методы предотвращения ветвления? - person spyr03; 28.03.2017
comment
Распределение выполняется быстрее, чем обнуление, но распределения не обнуляются. Итак, если вы выделяете каждый проход, вы должны сделать и то, и другое (при условии, что вы также хотите обнулить выделенную память) - person Donnie; 28.03.2017
comment
Обратите внимание, что мой ответ предлагает НИКОГДА ничего не перераспределять и обнулять только действительно ИСПОЛЬЗУЕМУЮ часть ленты. Это минимальное усилие, необходимое для инициализации новой ленты. Вы также можете сначала запомнить входную строку, а затем обнулить только оставшуюся часть ленты (использовавшуюся в предыдущем моделировании TM). - person stgatilov; 28.03.2017
comment
Избегайте push_back как чумы во время моделирования. Если вектор должен расти, а места нет, то копируется все. Это идет рука об руку с отсутствием аллокаций, но я хотел объяснить, почему это потенциально ужасно. (Особенно на огромном векторе) - person Donnie; 28.03.2017
comment
Чтобы избежать ветвей, вам нужно: мин/макс без ветвей. Обратите внимание, что ветвление для проверки state == halt должно быть сохранено как есть, поскольку оно хорошо предикативно (происходит только один раз за симуляцию). Ветку для move == 0 следует удалить. - person stgatilov; 28.03.2017
comment
Я не могу вспомнить, есть ли у Intel код операции без ответвления меньше, чем код операции, но вы можете сделать min/max без проверки ‹ в выражении. hbfs.wordpress.com/2008/08/05 / - person Donnie; 28.03.2017

Вот еще один ответ с более алгоритмическими подходами.

Моделирование по блокам

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

Разделите ленту на блоки по 8 символов в каждом. Каждый такой блок может быть представлен 16-битным числом (по 2 бита на символ). Теперь представьте, что автомат находится либо на первом, либо на последнем символе блока. Тогда его дальнейшее поведение зависит только от его начального состояния и начального значения на блоке, пока ТМ не выйдет из блока (либо влево, либо вправо). Мы можем предварительно вычислить результат для всех комбинаций (значение блока + состояние + конец) или, возможно, лениво вычислить их во время моделирования.

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

//R = table[s][e][V] --- outcome for TM which:
//  starts in state s
//  runs on a tape block with initial contents V
//  starts on the (e = 0: leftmost, e = 1: rightmost) char of the block
//The value R is a bitmask encoding:
//  0..15 bits: the new value of the block
//  16..17 bits: the new state
//  18 bit: TM moved to the (0: left, 1: right) of the block
//  ??encode number of steps taken??
uint32_t table[2][2][1<<16];

//contents of the tape (grouped in 8-character blocks)
uint16_t tape[...];

int pos = 0;    //index of current block
int end = 0;    //TM is currently located at (0: start, 1: end) of the block
int state = 0;  //current state
while (state != 2) {
  //take the outcome of simulation on the current block
  uint32_t res = table[state][end][tape[pos]];
  //decode it into parts
  uint16_t newValue = res & 0xFFFFU;
  int newState = (res >> 16) & 3U;
  int move = (res >> 18);
  //write new contents to the tape
  tape[pos] = newValue;
  //switch to the new state
  state = newState;
  //move to the neighboring block
  pos += (2*move-1);
  end = !move;
  //avoid getting out of tape on the left
  if (pos < 0)
      pos = 0, move = 0;
}

Проблема с остановкой

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

Первый тип зависания, который можно обнаружить, это когда машина остается на одном месте, не отходя далеко от него. Давайте сохраним окружение ТМ во время симуляции, которое представляет собой значения сегмента символов на расстоянии ‹ 16 от текущего местоположения ТМ. Если у вас есть 3 символа, вы можете закодировать окружение в 62-битном числе.

Поддерживайте хеш-таблицу для каждой позиции TM (как мы увидим позже, необходима только 31 таблица). После каждого шага сохраняйте кортеж (состояние, окружение) в хеш-таблице текущей позиции. Теперь важная часть: после каждого хода очищайте все хэш-таблицы на расстоянии >= 16 от TM (на самом деле нужно очистить только одну такую ​​хеш-таблицу). Перед каждым шагом проверяйте, присутствует ли (состояние, окружение) в хеш-таблице. Если это так, то машина находится в бесконечном цикле.

Вы также можете обнаружить другой тип зависания: когда машина бесконечно движется вправо, но никогда не возвращается назад. Для этого вы можете использовать одни и те же хеш-таблицы. Если ТМ находится на текущем последнем символе ленты с индексом p, то проверить текущий кортеж (состояние, окружение) не только в p-й хеш-таблице, но и в (p-1)-й, (p-2)-й, ..., (p-15)-й хэш столы. Если вы найдете совпадение, то TM находится в бесконечном цикле, двигаясь вправо.

person stgatilov    schedule 29.03.2017
comment
Был рассмотрен действительно хороший ответ, имитирующий несколько шагов одновременно, но я понятия не имел, как это сделать на самом деле. Спасибо и за ссылку. Что касается зацикливания TM, я делаю предварительную проверку, моделируя TM для ~ 10000 шагов, и проверяю, достигает ли он одного и того же внутреннего состояния более одного раза (голова, текущее_состояние, лента) с набором. Это не затронуло ТМ, которые работают вечно, так что это будет серьезное улучшение. - person spyr03; 29.03.2017

Изменять

int move = data % 2;

To

int move = data & 1;

Один — деление, другой — битовая маска, оба должны давать 0 или 1 в младшем бите. Вы можете сделать это в любое время, когда у вас есть % степени двойки.

Вы также устанавливаете

cell = tape[head];
data = TM[current_state][cell]; 
int move = data % 2;
int write = (data % 10) % 3;
current_state = data / 10;

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

    if(current_state == halt_state) {
        // This highlights the last place that is written to in the tape
        tape[head] = 4;
        vector<int> res = shorten_tape(tape);
        res.push_back(i+1);
        return res;
    }

^ Этот код не ссылается на «перемещение» или «запись», поэтому вы можете поместить вычисление для «переместить»/«записать» после него и вычислить их только в том случае, если current_state != halt_state

Также истинная ветвь оператора if является оптимизированной ветвью. Проверив состояние не остановки и поместив условие остановки в ветвь else, вы можете немного улучшить предсказание ветвления ЦП.

person Jason Lang    schedule 28.03.2017
comment
Я могу ошибаться, но не будут ли в наши дни все компиляторы автоматически переписывать мод как AND? Это похоже на то, что вряд ли внесет большой вклад, поскольку я сомневаюсь, что это узкое место. - person templatetypedef; 29.03.2017
comment
компилятор не сделает эту оптимизацию за вас. Это потому, что % и & имеют разное поведение для отрицательных чисел. % оставляет знак включенным, поэтому %2 от -1 равно -1, тогда как & возвращает только выбранные биты. Если вам нужно знать нечетное/четное и вы не хотите беспокоиться об отрицательных значениях, %2 небезопасно, и компилятор не сделает эту замену на случай, если вы передадите туда отрицательное значение (что означало бы это дало разные результаты). - person Jason Lang; 29.03.2017
comment
Кстати, я протестировал цикл из 2 миллиардов %2 против 2 миллиардов и 1. Visual C++ 2015, полная оптимизация скорости. Результатом стала экономия 20 секунд. 55 секунд для цикла %2 и 33 секунды для цикла &1. Таким образом, если вы выполняете 1 миллиард шагов, и каждый шаг выполняет %2, вы можете сэкономить 10 секунд на каждом прогоне, просто заменив %2 на &1. - person Jason Lang; 29.03.2017
comment
Да, у меня сложилось впечатление, что в C++ поведение mod при отрицательных значениях определяется реализацией и не должно было округляться таким образом. Я честно очень удивлен! - person templatetypedef; 29.03.2017
comment
Ну, это может варьироваться в зависимости от реализации, так что это еще одна причина предпочесть &1 для проверки нечетности/четности. Мне очень нравятся видеоролики Майка Эктона об оптимизации, они заставляют вас думать иначе. Дело в том, что многие оптимизации, которые мы делаем, зависят от знаний или предположений, специфичных для предметной области. Поэтому компилятор на самом деле не может их сделать. Люди слишком верят в магию компилятора, а не в хороший дизайн данных. - person Jason Lang; 29.03.2017
comment
Кстати, я неправильно понял десятичную точку, это было не отношение 33 к 55, это было отношение 3,3 к 55. Таким образом, добавление 1 миллиарда &1 + занимает около 1,5 секунды, но 20 секунд при использовании% 2. Отдохните около 20 секунд, чтобы пробежать 1 миллиард шагов. - person Jason Lang; 29.03.2017
comment
Это действительно интересно! Итак, если бы я использовал числовой тип без знака, смог бы компилятор (последовательно) заменить %2 на &1? - person spyr03; 29.03.2017