#c #performance #operator-overloading #operators #operator-keyword
#c #Производительность #перегрузка оператора #операторы #operator-ключевое слово
Вопрос:
В настоящее время я работаю над программой, в которой моя цель — как можно быстрее выполнять множество операций между двумя объектами (одного класса), используя как можно меньше места для хранения.
class number
{
public:
number(int x) :x{x} {};
int x;
// option 1
number operator (number x)
{
return number(this->x x.x);
};
// option 2
static void add(number* a, number* b, number* dest)
{
dest->x = a->x b->x;
};
};
int main()
{
number a(2);
number b(2);
number c(0);
// 4,608e-8 sec
c = a b;
// 2,318e-8 sec
number::add(amp;a,amp;b,amp;c);
}
Я рассмотрел два варианта:
- использование фактического оператора
- использование статической функции, принимающей три переменные (включая назначение) в качестве параметров
Первая лучше всего читается, но ее использование в больших масштабах может означать много необходимого места, потому что при каждом запуске инициализируется новый объект.
Возможно, я исправил это с помощью варианта 2. Принимая указатель на место назначения и, таким образом, повторно используя пространство для хранения. Вариант 2 довольно громоздкий для чтения, и если за другим происходит больше операций, код может быть очень сложным для понимания.
Я провел пару тестов скорости и пространства. Использование фактической функции оператора занимает 4,6 e-8 секунд на прогон и занимает 920 КБ памяти. Пустота занимает 2,3e-8 и 915 КБ.
Есть ли какой-либо вариант, который мне не хватает? Если нет, то какой из них является лучшим компромиссом между объемом памяти, скоростью и удобочитаемостью?
Комментарии:
1. Можете ли вы показать код, который вы используете для тестирования этого? Также вы включили оптимизацию?
2. Вы говорите о разнице в 0,00000002 секунды. Подобная задержка может быть вызвана практически чем угодно.
3. Беспокоитесь об одном вызове? Действительно? Это преждевременная оптимизация по учебнику, просто используйте operator, если это имеет смысл с точки зрения дизайна. Нет, не будет никакого нового объекта, вероятно, из-за исключения копирования, а если и было, ну и что? Это и int, вы не беспокоитесь о создании дополнительного int. Пусть компилятор позаботится о создании оптимизированного кода. Вы все равно не измеряете размер этой программы, есть тонна багажа, который может попасть в конечный ELF на основе флагов компиляции.
4. Напишите код самым простым, удобным для чтения и поддержки способом, который, по вашему мнению, будет соответствовать требованиям проекта. Протестируйте ее. Соответствует ли она требованиям? Если да, то все готово. Переходите к следующей задаче или идите домой пораньше и отдохните. Если он не профилирует и не изолирует узкие места. Усложняйте по мере необходимости, чтобы улучшить ОДНО узкое место. Тест. Если требования теперь выполнены, готово. Если не профилировать, изолировать и устранить одно узкое место. (или отмените последнее изменение, если вы сделали его хуже или не улучшили производительность настолько, чтобы это стоило дополнительных сложностей).
5. неясно, чего вы хотите достичь. Честно говоря, также код не демонстрирует это, потому что для добавления
int
s вы должны использоватьint
s и встроенный, вы не получите намного быстрее и эффективнее памяти, чем это
Ответ №1:
«Правильный» способ беспокоиться о производительности — включить оптимизацию компилятора. Большую часть времени это так.
Напишите правильный и читаемый код. Как только у вас это будет, вы сможете это измерить. Если вы обнаружите, что один фрагмент кода стоит дорого, вы можете посмотреть на сгенерированную сборку, чтобы понять, почему она стоит дорого.
Для добавления двух целых чисел наиболее кратким является:
int main() {
return 2 2;
}
Вывод компилятора (gcc 9.2 с -O3):
main:
mov eax, 4
ret
Теперь вы можете захотеть обернуть целые числа в класс:
struct number {
int x;
};
int main() {
return number{2}.x number{2}.x;
}
Это увеличивает стоимость создания экземпляров класса и доступа к их элементу. Компиляторы выводят:
main:
mov eax, 4
ret
Вместо прямого использования встроенного
вы можете использовать operator
:
struct number {
int x;
number operator (const numberamp; other){
return {x other.x};
}
};
int main() {
return (number{2} number{2}).x;
}
Это увеличивает стоимость вызова оператора. Компиляторы выводят:
main:
mov eax, 4
ret
Ваша версия со статическим методом (с минимальными изменениями) такова:
struct number {
int x;
static void add(number* a, number* b, number* dest) {
dest->x = a->x b->x;
};
};
int main()
{
number a{2};
number b{2};
number c{0};
number::add(amp;a,amp;b,amp;c);
return c.x;
}
Это увеличивает затраты вызывающего абонента на «подготовку» возвращаемого значения, поскольку оно является параметром outparameter . Это увеличивает стоимость косвенного обращения с помощью указателей. Это увеличивает стоимость использования static
метода, который раздувает класс. Компиляторы выводят:
main:
mov eax, 4
ret
Вывод: не делайте преждевременных оптимизаций. В приведенном выше примере все затраты, которые вы платите, находятся в вашем коде и не влияют на результирующую сборку. Напишите читаемый и простой код и оставьте оптимизацию mirco компилятору. Если вы написали правильный код, измерили его и поняли, что есть узкое место, тогда, конечно, вы можете попытаться улучшить эту часть. Хотя, прежде чем делать что-либо из этого, пытаться улучшить ваш number
для наиболее общего варианта использования бесполезно. Компилятор намного лучше в этом.
PS Что касается ваших чисел (4,608e-8 сек против 2,318e-8 сек), я должен признать, что я их проигнорировал. e-8 сек слишком мало, чтобы быть значительным. Также для бенчмарка вам нужно будет предоставить точный код бенчмарка, подробную информацию о том, какой компилятор вы использовали с какими опциями, и указать, на каком оборудовании вы его запускали. Честно говоря, без этих деталей цифры бессмысленны.