Я создал интерпретатор в C++ для языка, созданного мной.
Одна основная проблема в дизайне состояла в том, что у меня было два различных типов на языке: число и строка. Таким образом, я должен раздать структуру как:
class myInterpreterValue
{
myInterpreterType type;
int intValue;
string strValue;
}
Объекты этого класса являются розданным миллионом раз секунда во время, например: цикл обратного отсчета на моем языке.
На профилирование указывают: 85% производительности ест функция выделения строкового шаблона.
Это довольно ясно мне: Мой интерпретатор имеет плохой дизайн и не использует указатели достаточно. Все же у меня нет опции: Я не могу использовать указатели в большинстве случаев, поскольку я просто должен сделать копии.
Как сделать что-то против этого? Является класс как это лучшей идеей?
vector<string> strTable;
vector<int> intTable;
class myInterpreterValue
{
myInterpreterType type;
int locationInTable;
}
Таким образом, класс только знает то, что вводит его, представляет и положение в таблице
Это однако снова имеет недостатки: я должен был бы добавить временные ценности к таблице векторов строки/интервала и затем удалить их снова, это съест большую производительность снова.
Я подозреваю, что многие значения не являются строками. Итак, первое, что вы можете сделать, это избавиться от объекта string
, если он вам не нужен. Объедините это в союз. Другое дело, что, вероятно, многие из ваших строк очень маленькие, поэтому вы можете избавиться от выделения кучи, если сохраните маленькие строки в самом объекте. В LLVM для этого есть шаблон SmallString
. И затем вы можете использовать интернирование строк, как говорится в другом ответе. В LLVM для этого есть класс StringPool
: вызовите intern ("foo")
и получите умный указатель, ссылающийся на общую строку, потенциально используемую другими объектами myInterpreterValue
. .
Объединение можно записать так:
class myInterpreterValue {
boost::variant<int, string> value;
};
boost :: variant
выполняет тегирование типов за вас. Вы можете реализовать это так, если у вас нет наддува. Выравнивание пока не может быть перенесено в C ++, поэтому мы помещаем некоторые типы, которые, возможно, требуют большого выравнивания, в объединение хранилищ.
class myInterpreterValue {
union Storage {
// for getting alignment
long double ld_;
long long ll_;
// for getting size
int i1;
char s1[sizeof(string)];
// for access
char c;
};
enum type { IntValue, StringValue } m_type;
Storage m_store;
int *getIntP() { return reinterpret_cast<int*>(&m_store.c); }
string *getStringP() { return reinterpret_cast<string*>(&m_store.c); }
public:
myInterpreterValue(string const& str) {
m_type = StringValue;
new (static_cast<void*>(&m_store.c)) string(str);
}
myInterpreterValue(int i) {
m_type = IntValue;
new (static_cast<void*>(&m_store.c)) int(i);
}
~myInterpreterValue() {
if(m_type == StringValue) {
getStringP()->~string(); // call destructor
}
}
string &asString() { return *getStringP(); }
int &asInt() { return *getIntP(); }
};
Вы уловили идею.
Я думаю, что некоторые динамические языки кэшируют все эквивалентные строки во время выполнения с поиском по хешу и хранят только указатели. Таким образом, на каждой итерации цикла, где строка остается неизменной, будет только присвоение указателя или, самое большее, функция хеширования строки. Я знаю, что некоторые языки (кажется, Smalltalk?) Делают это не только со строками, но и с небольшими числами. См. Модель наилегчайшего веса .
IANAE по этому поводу. Если это не помогает, вы должны указать код цикла и объяснить, как он интерпретируется.
И в Python, и в Ruby целые числа являются объектами. Так что вопрос не в том, что «значение» является целым числом или строкой, это может быть что угодно. Кроме того, все, что написано на обоих этих языках, собирается сборщиком мусора. Нет необходимости копировать объекты, указатели можно использовать внутри, если они безопасно хранятся там, где их увидит сборщик мусора.
Итак, одно из решений вашей проблемы:
class myInterpreterValue {
virtual ~myInterpreterValue() {}
// example of a possible member function
virtual string toString() const = 0;
};
class myInterpreterStringValue : public myInterpreterValue {
string value;
virtual string toString() const { return value; }
};
class myInterpreterIntValue : public myInterpreterValue {
int value;
virtual string toString() const {
char buf[12]; // yeah, int might be more than 32 bits. Whatever.
sprintf(buf, "%d", value);
return buf;
}
};
Затем используйте виртуальные вызовы и dynamic_cast
для включения или проверки типов вместо сравнения со значениями myInterpreterType.
Обычно на этом этапе нужно беспокоиться о том, что вызовы виртуальных функций и динамическое приведение могут быть медленными. И Ruby, и Python повсюду используют вызовы виртуальных функций. Хотя и не виртуальные вызовы C ++: для обоих языков их "стандартная" реализация находится на C с настраиваемыми механизмами полиморфизма. Но в принципе нет оснований предполагать, что «виртуальный» означает «производительность вне окна».
Тем не менее, я полагаю, что у них обоих, вероятно, есть некоторые умные оптимизации для определенного использования целых чисел, в том числе в качестве счетчиков циклов. Но если вы в настоящее время видите, что большая часть вашего времени тратится на копирование пустых строк, тогда вызовы виртуальных функций путем сравнения почти мгновенны.
Настоящее беспокойство заключается в том, как вы собираетесь управлять ресурсами - в зависимости от ваших планов в отношении интерпретируемого языка, сборка мусора может доставить больше проблем, чем вы хотите.
Самый простой способ решить эту проблему - сделать его указателем на строку и выделять его только при создании строкового значения. Вы также можете использовать объединение для экономии памяти.
class myInterpreterValue
{
myInterpreterType type;
union {
int asInt;
string* asString;
} value;
}