таблица функций vs переключатель в golang

im пишу простой эмулятор в go (должен ли я? или мне вернуться в Си?). так или иначе, я забираю инструкцию и расшифровываю ее. на данный момент у меня есть байт, как 0x81, и я должен выполнить правильную функцию.

должен ли я иметь что-то подобное

func (sys *cpu) eval() {
    switch opcode {
    case 0x80:
        sys.add(sys.b)
    case 0x81:
        sys.add(sys.c)
    etc
    }
}

или что-то вроде этого

var fnTable = []func(*cpu) {
    0x80: func(sys *cpu) {
        sys.add(sys.b)
    },
    0x81: func(sys *cpu) {
        sys.add(sys.c)
    }
}
func (sys *cpu) eval() {
    return fnTable[opcode](sys)
}

1.какой из них лучше?
2.какой из них быстрее?
также
3.я могу объявить функцию как inline?
4.у меня есть cpu struct, в котором у меня есть регистры и т. д. было бы быстрее, если бы у меня были регистры и все как глобалы? (без struct)

большое спасибо.

3 ответов


  1. первая версия выглядит лучше для меня, YMMV.

  2. критерии. Зависит от того, насколько хорош компилятор при оптимизации. Версия "jump table" может быть быстрее, если компилятор недостаточно старается оптимизировать.

  3. зависит от вашего определения того, что такое "объявить функцию inline". Go может объявлять и определять функции / методы только на верхнем уровне. Но функции-граждане первого класса в Go, поэтому можно иметь переменные / параметры / возвращаемые значения и структурированные типы типов функций. Во всех этих местах функция literal можно [также] назначить переменной/полю/элементу...

  4. возможно. Тем не менее я бы предложил не сохранять состояние cpu в глобальной переменной. Как только вы, возможно, решите эмулировать multicore, это будет приветствоваться ; -)


Я сделал несколько тестов, и версия таблицы быстрее, чем версия коммутатора, как только у вас будет более 4 случаев.

Я был удивлен, обнаружив, что компилятор Go (gc, во всяком случае, не уверен в gccgo), похоже, недостаточно умен, чтобы превратить плотный коммутатор в таблицу перехода.

обновление: Кен Томпсон опубликовал в списке рассылки Go описание трудности оптимизации переключатель.


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

например, учитывая такой ast:{* (a, {+ (b, c)})}

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

func (e *evaluator) compile(brunch ast) {
    switch brunch.type {
    case binaryOperator:
        switch brunch.op {
        case *: return func() {compile(brunch.arg0) * compile(brunch.arg1)}
        case +: return func() {compile(brunch.arg0) + compile(brunch.arg1)}
        }
    case BasicLit: return func() {return brunch.arg0}
    case Ident: return func(){return e.GetIdent(brunch.arg0)} 
    }
}

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