Оптимизировать цикл вокруг get и вызова функции с использованием возвращаемого значения?

#c #pass-by-reference #pass-by-value #temporary

#c #передача по ссылке #передача по значению #временное

Вопрос:

Вот фрагмент, получающий данные из буферизованного источника и отправляющий их для обработки. Если очередь пуста, get() возвращает null, и метод process с радостью принимает null и ничего не делает. Какой наиболее оптимальный способ закодировать это?

 something a; // any legal C   return type...
aQueueOfSomethings g;

while (true) { 
    a=g.get();
    process(a);
}
  

Невозможно предсказать значения, поступающие через get(), они такие, какие они есть, и их нужно удалить из очереди и передать process () как можно быстрее.

Я не вижу здесь много потраченных впустую усилий — если я пропущу явную локальную переменную с именем ‘a’ и сделаю цикл однострочным:

     process(g.get());
  

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

Если возвращаемая вещь имеет какой-либо размер или сложность, было бы лучше иметь указатель на нее, а не ее копию, и передавать этот указатель, а не копировать по значению… Поэтому я бы предпочел иметь

 something *a;

    g.get(a);
    process(a);
  

вместо

  something a;

    a=g.get();
    process(a);
  

Я написал тестовый пример на c , попробовав двухстрочную и однострочную версии, выполнив цикл 100 000 000 раз.

Если a является объектом с 4 целыми числами и 2 числами с плавающей запятой, и метод process() затрагивает их все, двухстрочное решение на самом деле быстрее! Если объект a представляет собой один int, однострочная версия работает быстрее. Если объект сложный, но метод process() касается только одного значения, однострочная версия работает быстрее.

Самое интересное для меня, что при использовании компилятора g , Mac OS X 10.5.8, переключатель оптимизации первого уровня -O приводит к идентичной, намного более быстрой работе как с 1, так и с 2-строчными версиями.

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

Комментарии:

1. «Ах, мотивация!?» Рад, что вы спросили. Это был вопрос из интервью, и, перейдя к форме из 2 строк, я сказал, что, по-моему, мы закончили. Человек, с которым я разговаривал, был очень удивлен и твердо сказал, что я чего-то не понимаю. Что-то настолько очевидное, что она не сказала мне, что это было, я должен подумать об этом. Итак, у меня есть. (Очевидно, я не смог произвести впечатление и больше не являюсь кандидатом на эту работу …) Вот почему мне было интересно узнать об однострочной версии — и почему я создал и измерил тестовые примеры. Оптимизация вызывает возражения. Возможно, она имела в виду, что я должен изменить интерфейс на передачу по ссылке.

Ответ №1:

Я думаю, что это высший случай бесполезной оптимизации

(вы берете что-то, что буферизует, и хотите немного оптимизировать это?)

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

В сочетании с вероятным внедрением queue_class::get() ваша проблема кажется совершенно СПОРНОЙ

Ответ №2:

Я полагаю, что вы пытаетесь превзойти компилятор в его собственной работе.

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

Комментарии:

1. Я тоже так думал. Я рад читать, что другие видят это именно так!

Ответ №3:

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

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

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

Комментарии:

1. 1 точно. Я собирался предложить, что очевидным способом ускорить этот цикл было бы его распараллеливание. Я почти уверен, что это не то, что имел в виду исходный вопрос, конечно.

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

Ответ №4:

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

 while (true) { 
    a=g.get();
    b=g.get();
    c=g.get();
    d=g.get();
    process(a);
    process(b);
    process(c);
    process(d);
}
  

тогда это могло бы ускорить работу.

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

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

Комментарии:

1. Это может (всегда ориентируйтесь, если вам интересно) повысить пропускную способность, если есть накопившиеся данные для обработки, но увеличит задержку, когда только первые get() возвращаемые данные….

2. Да, всегда бенчмарк 🙂 Верно: это оптимизация пропускной способности, а не задержки.

3. 1 за творческое мышление; жаль, что в данном случае это, вероятно, академично 🙂

4. Это может окупиться — переключение между двумя разными частями кода может быть медленнее, чем выполнение нескольких вызовов для одной части и нескольких вызовов для другой части. Мне также с опозданием приходит в голову, что многопоточность с одним потоком, считывающим g.get(), и одним потоком, обрабатывающим(), с одной переменной для каждого, с большой вероятностью будет выполняться быстрее, чем последовательный однопоточный. Затем существуют очереди с буферизацией FIFo… Заявленной целью был максимальный поток, а не кратчайшее время передачи.