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)

C и C++ не функциональные языки, чтобы test(a) == test(a) всегда было true, поэтому gcc, должно быть, очень умный если умеет такое доказывать или опровергать для любого test(); Ну а про то как делать мемоизацию в C++ в обобщенном виде написано, например, на SO: http://stackoverflow.com/questions/17805969/writing-universal-memoization-fun... Вопрос - при чем тут компиляторы?

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

поэтому gcc, должно быть, очень умный если умеет такое доказывать или опровергать для любого test();

GCC как раз этого делать не умеет. Он сворачивает это в return 1; только если ему явно сказать, что функция чистая. Но тогда мемоизация не работает.

Вопрос - при чем тут компиляторы?

Компиляторы делают оптимизации. Я хочу делать свой язык и компилировать его в Си, потом код на Си компилировать компилятором Си. Все испробованные мною компиляторы Си не умеют в оптимизациию, которая мне нужна. Вот причем. Думаю, надо в компилятор систему символьных вычислений интегрировать. Абстрактное представление LLVM мне не очень нравится, на мой взгляд оно не очень подходит для символьных вычислений

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

ну ты понял идею

struct Test {
  mutable uint32_t lookup[256];
  uint32_t pow4(uint8_t a) const {
    return lookup[a] ? lookup[a] :
  	(lookup[a] = a * a * a * a);
  }
};
mix_mix ★★★★★
()
Последнее исправление: mix_mix (всего исправлений: 2)
Ответ на: комментарий от SZT

Я хочу делать свой язык и компилировать его в Си

Ну так и сделай эту оптимизацию на Си, зачем рассчитывать на какие-то конкретные компиляторы?

asaw ★★★★★
()
Ответ на: ну ты понял идею от mix_mix

С оптимзиацией не работает. На выходе получаю

	mov	eax, 1
	ret
хотя оно должно еще мне в этот mutable массив записать значение.

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

Тогда мне надо уже свой оптимизатор делать, а не просто тупо транслировать код из моего_гипотетического_языка в Си и рассчитывать уже на оптимизацию в самом Си. Это сильно все усложняет. Если делать оптимизатор, тогда я лучше вообще не буду C/C++ использовать, а буду состыковываться с LLVM или GCC непосредственно.

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

А, я понял что ты хочешь. Боюсь, это из коробки невозможно.

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

Он сворачивает это в return 1; только если ему явно сказать, что функция чистая

Что логично для чистой функции. Но у тебя-то не чистая функция. Каким образом компилятор должен догадаться, что 1.Результат строго зависит от параметра, 2.Побочный эффект достаточно воспроизвести только один раз? Ты хочешь очень специфическую магию, как мне кажется.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Что логично для чистой функции. Но у тебя-то не чистая функция.

Считать такую функцию чистой или не чистой - это вопрос терминологии.

1) Побочный эффект этой функции никак не может влиять на работу каких-либо других функции (т.к. ее массив доступен на чтение/запись только ей)

2) Побочные эффекты этой функции никак не влияют на значения, возвращаемые фунцкией. Они влияют только на время выполнения(вычисления).

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

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

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

Кто некомпетентной никчемности дал право кукарекать об оптимизация/конпеляторах? Жри говно и не кукарекай.

Тогда мне надо уже свой оптимизатор делать

Куллстори, как буд-то бы что-то можешь.

а не просто тупо транслировать код из моего_гипотетического_языка в Си

Посмеялся.

Если делать оптимизатор, тогда я лучше вообще не буду C/C++ использовать, а буду состыковываться с LLVM или GCC непосредственно.

Ещё раз посмеялся.

Твой высер говно, твоя псевдоидея говно. Ты даже высрать это говно вменяемо не смог.

Ну и давай контрольный:

test3:
.LFB4941:
	.cfi_startproc
	testb	%dil, %dil
	jne	.L20
.L19:
	movl	$1, %eax
	ret
	.p2align 4,,10
	.p2align 3
.L20:
	movzbl	%dil, %edi
	leal	-1(%rdi), %eax
	cltq
	movl	lookup.30604(,%rax,4), %edx
	testl	%edx, %edx
	jne	.L19
	imull	%edi, %edi
	imull	%edi, %edi
	movl	%edi, lookup.30604(,%rax,4)
	movl	$1, %eax
	ret
	.cfi_endproc

Высер из разряда «я у мамы аутист».

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

Даже если рассмотреть однозначно чистую

Ну дак и иди в мир эльфов - там всё просто. Авось там с братьями-ламерками будешь чуть больше, чем никчемной балаболкой.

anonymous
()

Эээээ... Что считать «чистой» функцией?

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

Побочный эффект этой функции никак не может влиять на работу каких-либо других функции (т.к. ее массив доступен на чтение/запись только ей)

Это C. Я могу присунуть в этот массив из другой функции угадав смещение.

Побочные эффекты этой функции никак не влияют на значения, возвращаемые фунцкией. Они влияют только на время выполнения(вычисления).

Это C. Тут нет понятия побочных эффектов. Тут все один большой побочный эффект.

kirk_johnson ★☆
()

Я думал насчет того чтобы создать свой язык программирования и «компилировать» его в исходик C и потом его компилить C компилятором

Haskell?

kirk_johnson ★☆
()

Увы, в C и C++ нет и не будет ссылочной прозрачности.

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

Это C. Я могу присунуть в этот массив из другой функции угадав смещение.

Это UB.

Это C. Тут нет понятия побочных эффектов. Тут все один большой побочный эффект.

В GCC можно объявить pure и const функции, у которых побочных эффектов быть не должно

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

Это UB.

ШТОА?! Это называется запись по указателю. Вполне себе DB.

В GCC можно объявить pure и const функции, у которых побочных эффектов быть не должно

В которой я сделаю system(«poweroff») и буду наслаждаться махровейшим побочным эффектом. Ещё раз - в C _НЕТ_ концепции побочных эффектов. У тебя _НЕТ_ возможности сказать, чистая функция или нет. Возьми другой язык.

kirk_johnson ★☆
()

Два вызова это проблема разве? Первый сделает расчёт, второй прочитает таблицу. Если реально считать что-то сложнее четвёртой степени, оверхед абсолютно ничтожный.

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

ШТОА?! Это называется запись по указателю.

Как ты без UB получишь указатель на static массив, объявленный внутри функции, если сама функция не предусматривает никакого способа вытащить «наружу» этот указатель? Кроме того, компилятор имеет право вообще выкинуть static массив.

Например,

uint32_t test(uint8_t a)
{
  static uint32_t lookup[3] = {1,2,3};
  return lookup[0];
}
откомпилируется в:
	mov	eax, 1
	ret
И никакого указателя нет т.к. самого массива нет. Можно разве что код функции пропатчить, чтобы оно вместо 1 что-то другое возвращало, но это уже совсем другая тема.

В которой я сделаю system(«poweroff») и буду наслаждаться махровейшим побочным эффектом.

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

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

Как ты без UB получишь указатель на static массив, объявленный внутри функции, если сама функция не предусматривает никакого способа вытащить «наружу» этот указатель?

Эээ... UB - это когда стандартом не определено. Указатели в стандарте определены.

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

Нет, не должен. В этом и состоит прекрасная идея зашить побочные эффекты в типы языка. Внезапно, Haskell.

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

Кроме того, компилятор умеет «доказывать» чистоту функций в некоторых случаях.

Приведи мне, пожалуйста, примеры таких случаев.

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

http://goo.gl/wlWRES вот пример. Из ассемблерного листинга видно, что функция test вызывается из main лишь единожды. Таких примеров можно очень много придумать

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

Эээ... UB - это когда стандартом не определено. Указатели в стандарте определены.

Как ты указатель получишь без UB, если функция не отдает указатель на static массив ни в каком виде? Кроме того, в скомпилированном бинарнике этого static «массива» может вообще не быть, и указателя на него никак не получить.

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

Наоборот. Он просто соптимизировал вызовы и функция стала noop. Я хотел тебе показать, что компилятор не может разобрать хоть сколько-нибудь сложные случаи, потому что в C ссылки на функции - это просто поинтер на память. Ровно как и любые другие ссылки. А следовательно широкого поля для применения той оптимизации, который ты хочешь - нет.

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

В таком случае можно улучшить компилятор, чтобы он умел более сложные случаи оптимизировать. Или лучше вообще придумать новый ЯП. Haskell, который ты упоминал, не подходит по причине GC, жирного рантайма и соответственно невозможности запихнуть этот Haskell в микроконтроллеры, например. Кроме того, код на C зачастую быстрее https://ro-che.info/ccc/images/safety.png

Я хочу сделать язык на основе принципа, описанного тут inline callback-функций как способ метапрограммирования без всяких обязательных GC и навязывания чисто-функционального стиля

SZT ★★★★★
() автор топика

Это оптимизация под очень-очень редкий случай. Обычно в С и С++ запоминают результаты «длинных» вычислений или использующих загрузку данных. А там эта оптимизация как собаке пятая нога.

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

Чувааааак. Обращение по указателю - это уже побочный эффект. Потому что между вызовами я мог сделать unmap/mmap и там теперь другие данные.

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

Тогда и вызов вообще любой функции(даже той, которая ничего не делает) это побочный эффект. GCC считает, что ты не будешь делать таких вещей, как unmap/mmap регионов памяти, где расположен исполняемый код и статические массивы, использовать ptrace или /proc/self/mem для изменения исполняемого кода. Ведь этот http://goo.gl/zWzqDS код он смог заоптимизировать и понять, что там нет никаких побочных эффектов

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

А на каких задачах хаскель может обойти Си? http://research.microsoft.com/en-us/um/people/simonpj/papers/ndp/haskell-beat... вот например такое нагуглил.

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

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

А на каких задачах хаскель может обойти Си?

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

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

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

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

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

Если мы говорим о современных процессорах, я хотел бы посмотреть на того сверхчеловека, который способен писать код на ассемблере с учётом работы конвейера и существования нескольких уровней памяти. Серьёзно, использование ассемблера для оптимизации программ перестало быть актуальным лет 20 назад. Добро пожаловать в 2015 год.

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

я хотел бы посмотреть на того сверхчеловека, который способен писать код на ассемблере с учётом работы конвейера и существования нескольких уровней памяти

Для этого не нужно ничего сверхчеловеческого. Даже я могу.

Серьёзно, использование ассемблера для оптимизации программ перестало быть актуальным лет 20 назад.

До тех пор, пока не столкнёшься с обработкой мультимедиа.

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

До тех пор, пока не столкнёшься с обработкой мультимедиа.

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

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

Для C? Возможно. С другой стороны, какой-нибудь DSL, транслируемый в нужный набор команд процессора сильно упростит задачу.

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

Параллельная обработка, например.

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

Есть примеры задач, где даже Java, прости хоспади, быстрее C.

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

Современный конпеляторы генерят куда более вменяемый ассемблерный код, чем ты.

Нет. Могу привести конкретный пример невменяемого кода, сгенерированного компилятором см. 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 например. Так что смысл писать/оптимизировать на ассемблере есть.

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

С другой стороны, какой-нибудь DSL, транслируемый в нужный набор команд процессора сильно упростит задачу.

Есть какой-то специальный JIT-компилятор для SIMD http://code.entropywave.com/orc/

Но разрабы всяких ffmpeg x264 предпочитают ассемблер

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

Вообще-то этот orc может генерировать код на ассемблере, т.е. без JIT-а http://code.entropywave.com/documentation/orc/orc-concepts.html

The application developer uses Orc to produce assembly source code that is then compiled into the application. This requires the developer to have Orc installed at build time. The advantage of this method is no Orc dependency at runtime. Disadvantages are a more complex build process, potential for compiler incompatibilities with generated assembly source code, and any Orc improvements require the application to be recompiled.

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

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

Web-сервер пойдёт?

http://www.yesodweb.com/blog/2012/10/measuring-warp

TL;DR на одном потоке nginx заруливает раза в полтора, но дальше 5 воркеров nginx не скейлится (как это по-русски сказать?).

UPD: http://www.yesodweb.com/blog/2014/02/new-warp

Тут чуваки зарулили nginx даже на одном воркере. Но там вроде как старая версия nginx (1.4.0 на тот момент было уже почти год), так что я не уверен в честности сравнения, мб в nginx к тому времени тоже было лучше с производительностью.

при том чтобы на С нельзя было впринципе написать кода

Что это мать твою значит?

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

Вообще-то этот orc может генерировать код на ассемблере, т.е. без JIT-а

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

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

при том чтобы на С нельзя было впринципе написать кода

Что это мать твою значит?

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

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