LINUX.ORG.RU

Оптимизация функций с побочными эффектами(мемоизация)

 , , , ,


0

5

Почему современные C/C++ компиляторы не умеют как следует в оптимизацию функций с побочным эффектом? Взять например такой код

#include <inttypes.h>
#include <stdbool.h>

uint32_t test(uint8_t a)
{
  static uint32_t lookup[255] = {0};
  if (a == 0) return 0;
  if (lookup[a-1] != 0 ) return lookup[a-1];
  uint32_t tmp = a;
  tmp = tmp * tmp * tmp * tmp;
  lookup[a-1] = tmp;
  return tmp;
}

bool test2(uint8_t a)
{
  return test(a) == test(a);
}
Понятно, прироста производительности такая мемоизация на возведении в четвертую степень не даст. Но если считать что-то значительно более ресурсоемкое, и например использовать хеш-таблицу для поиска «считали ли мы уже такое», то прирост производительности определенно будет. Так вот, ни один проверенный мной компилятор не может нормально оптимизировать функцию test2. Если просто скомпилить это дело без всяких заморочек, компилятор дважды заинлайнит код функции test внутрь test2. Если сделать uint32_t test(uint8_t) __attribute__ ((noinline)); то функция будет вызвана дважды, и потом результаты будут сравниваться. А если сделать: uint32_t test(uint8_t) __attribute__ ((pure)); и компилить с -Os то в итоге код функции test2 свернется в аналог return 1; и никакой мемоизации я при этом не получу (см. http://goo.gl/YQNc8B )- это не то, что я хочу получить.

Так вот, к чему это я. Вопрос: возможно ли как-нибудь «убедить» существующе C(желательно) или на C++(нежелательно) компиляторы, чтобы они мне нормальную мемоизацию делали? Чтобы у меня в данном примере из функции test2 ОДИН РАЗ или вызывалась функция test(записывая или не записывая число в lookup массив) и возвращался 1 или чтобы инлайнил кусок кода из функции лишь единожды, для записи в static массив и чтобы возвращал 1; Я думал насчет того чтобы создать свой язык программирования и «компилировать» его в исходик C и потом его компилить C компилятором, но если компиляторы такие вещи не оптимизируют, мне придется лезть в SSA, LLVM и прочие подобные вещи, а такая перспектива меня не радует

★★★★★

Последнее исправление: SZT (всего исправлений: 1)

Ответ на: комментарий от SZT

Это значит «при том чтобы не существовало такого кода на С, скомпилированный бинарник из которого был бы быстрее бинарника, полученного посредством компиляции некоего Haskell-кода»

Как минимум будет существовать код на C, выдающий идентичный бинарник тому, который выдаст GHC для определённого кода на Haskell. Ну и наоборот, никто не мешает с помощью Haskell генерировать код на C. Последнее, кстати, довольно часто используется.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Ичо? GCC может генерить код на ассемблере. GHC может генерить код на ассемблере. Да блин, даже PHP можно скомпилировать в код на ассемблере.

А что тогда ты имел ввиду, когда писал

Потому что они ограничены C, ясен хрен.

Ничем они не ограничены, ничто и никто им не запрещает писать код под этот orc и хранить в репозиториях код на orc и/или код на ассемблере, который этот orc выдал

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

никто им не запрещает писать код под этот orc

Он уже портирован на все нужные платформы? К слову, в ffmpeg сотоварищи для любого кода на ассемблере есть эталонная реализация на C.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Как минимум будет существовать код на C, выдающий идентичный бинарник тому, который выдаст GHC для определённого кода на Haskell. Ну и наоборот, никто не мешает с помощью Haskell генерировать код на C.

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

SZT ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Он уже портирован на все нужные платформы?

Архитектуры? Нет. Например под ARM64 он не портирован. Но разве это мешает использовать его для создания кода на ассемблере под часть архитектур?

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

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

Кому не должен? Что не должен? Что ты вообще несёшь? Ты про метапрограммирование когда-нибудь слышал?

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

пфф. Я хочу сравнивать возможности по оптимизации у компиляторов Haskell и C, а не создавать на С и Haskell некий код генерирующий некий код или бинарник, который бы эту задачу решал. Разве это не очевидно? Попробуй в том же https://benchmarksgame.alioth.debian.org/ предложить в качестве решения некоей задачи код на хаскеле например, который при запуске создавал бы некий бинарник, запускал бы его на выполнение, и чтообы уже этот бинарник решал непосредственную задачу, интересно что тебе на такое ответят.

SZT ★★★★★
() автор топика
Последнее исправление: SZT (всего исправлений: 1)
Ответ на: комментарий от SZT

Параллельная обработка чего? Мне нужен конкретный пример задачи, в котором хаскель на параллельной обработке обгонет С, при том чтобы на С нельзя было впринципе написать кода, который бы обогнал код на хаскеле(рассматриваются актуальные версии компиляторов С и хаскель. Ассемблерные вставки и FFI не рассматриваются. Целевая архитектура x86-64. OS - GNU/Linux).

Написать можно что угодно, хоть на brainfuck'е. Вопрос в том, сколько времени ты готов потратить.

Насчет Java я еще могу поверить. Там в hotspot-е есть адаптивная оптимизация, когда ява-машина динамически перекомпилирует(адаптирует) код исходя из результатов рантайм-профилирования. Но разве в GHC есть нечто подобное?

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

Нет. Могу привести конкретный пример невменяемого кода, сгенерированного компилятором см. http://gcc.1065356.n5.nabble.com/Ways-to-fill-the-stack-td912561.html . Багрепорт -> https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66152

Могу привести конкретный пример невменяемого кода, написанного людьми. Что это докажет?

Практика показывает, что компиляторы не в сосотянии сами по себе(без специальных подсказок-интринсиков) генерировать эффективный код. Например http://blog.lexa.ru/2012/12/26/opyat_o_sovremennykh_cpu.html http://thedeemon.livejournal.com/49226.html .

Нет. Практика показывает, что они это делают не всегда.

Есть FFmpeg с кучей кода на ассемблере для разных архитектур https://github.com/FFmpeg/FFmpeg/search?l=gas&q=&utf8=✓ и еще libjpeg-turbo например. Так что смысл писать/оптимизировать на ассемблере есть.

А я говорил, что нету? :)

kirk_johnson ★☆
()
Ответ на: комментарий от SZT

Попробуй в том же https://benchmarksgame.alioth.debian.org/ предложить в качестве решения некоей задачи код на хаскеле например, который при запуске создавал бы некий бинарник, запускал бы его на выполнение, и чтообы уже этот бинарник решал непосредственную задачу, интересно что тебе на такое ответят.

Не знаю про Debian, но ребята из NASA сказали «отлично» и проспонсировали Copilot.

kirk_johnson ★☆
()
Ответ на: комментарий от kirk_johnson

Написать можно что угодно, хоть на brainfuck'е.

Brainfuck-компиляторов, генерирующих эффективный машинный код, не существует в природе. Писать оптимизирующий компилятор brainfuck нецелесообразно по вполне очевидным причинам. Так что код на brainfuck вряд ли когда-либо обгонит код на C или Haskell.

Вопрос в том, сколько времени ты готов потратить.

Это время в итоге может многократно окупиться при использовании программы.

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

Возможно

Могу привести конкретный пример невменяемого кода, написанного людьми. Что это докажет?

Это докажет, что человек может написать невменяемый код.

Мой же пример доказывает, что компилятор может написать невменяемый код. И для человека, который даже поверхностно знаком с ассемблером, невменяемость этого кода очевидна.

Нет. Практика показывает, что они это делают не всегда.

Согласен. Но в большинстве случаев человек способен таки улучшить результат, который получается после компиляции из C в ассемблер. Хотя бы потому, что человек не обязан следовать соглашениям вызова, которые приняты в конкретном компиляторе на конкретной архитектуре.

Взять например соглашения вызовов ARM https://en.wikipedia.org/wiki/Calling_convention#ARM_.28A32.29

Там говорится, что содержимое некоторых регистров должны быть точно такими же, как и перед вызовом функции

Subroutines must preserve the contents of r4 to r11 and the stack pointer. (Perhaps by saving them to the stack in the function prologue, then using them as scratch space, then restoring them from the stack in the function epilogue).

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

А я говорил, что нету? :)

Ты говорил:

Так что ассемблерные вставки с изрядной долей вероятности сделаю только хуже.

Я говорю, что пользуясь ассемблером и доделывая руками код, который выдал компилятор, вероятность получить лучший с точки зрения производительности код, при условии знания ассемблера и особенностей архитектуры процессора, и при условии что сам код не является тривиальным, стремится к 100%. Стоит ли овчинка выделки - это уже отдельная тема.

SZT ★★★★★
() автор топика
Ответ на: комментарий от kirk_johnson

Не знаю про Debian, но ребята из NASA сказали «отлично» и проспонсировали Copilot.

Да я тоже не имею ничего против кодогенерации и метапрограммирования. Но когда сравнивается эффективность оптимизаций в GHC и GCC, создание на хаскеле кода, генерирующего код на C и последующая компиляция этого кода через GCC это явно нечестный прием. Тогда это будет уже сравнение GCC и GCC, а не GHC и GCC.

SZT ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.