LINUX.ORG.RU

О хвостовой рекурсии

 


3

4

Или с чем ее едят и как ее готовят? Набрел на статью в Википедии, ознакомился, сделал сравнительный замер времени выполнения для приведенного там примера. Результаты впечатлили, особенно если учесть, что тестировал я это для Scala с включенной оптимизацией.пиляторы

Собственно вопросов два.

1. Хотелось бы получше разобраться в устройстве хвосто-рекурсивных функций и их вызовов, дабы объяснить человеку, далекому от этой темы - в чем там магия?

2. Какие еще популярные ЯП и компиляторы поддерживают оптимизацию данного вида рекурсии?

Всем спасибо.

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

в том-то и дело, что кроме RET

В том-то и дело, что никакого ret'а там нет. Из хвостового вызова вообще не происходит выхода. Сам вызов - это джамп (но обычно изза сложностей рантайма там некоторые добавки), а возврата нет. Просто нет. Никакого кода. Даже нопов нету. Пусто там :)

Видел, я в своём коде написал в начале функции int i=0 ? Это я выделил sizeof(int) char'ов из стека. А кто их возвращает в стек?

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

Потому JMP ты не отделаешься, нужно ещё SP пофиксить.

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

И ещё об оптимизации: твоя ТСО это конечно сильно, но это далеко не всё: сам цикл тоже можно развернуть, и тем достигнуть профита.

Как я написал выше - в случае ХР луп-анроллинг это банальный инлайнинг.

потому и кормим его Array и _одновременно_ Array[i+1]

Да, именно так и произойдет, когда будет сделано TCO после инлайнинга :)

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

т.е. без tl; никак?

для тебя — да.

достал.. show me the code.

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

В том-то и дело, что никакого ret'а там нет. Из хвостового вызова вообще не происходит выхода. Сам вызов - это джамп (но обычно изза сложностей рантайма там некоторые добавки), а возврата нет. Просто нет. Никакого кода. Даже нопов нету. Пусто там :)

Наверное, стоит написать подробнее. вот пример кода:

(define (f1 x) (f2 x))
(define (f2 x) (f3 x))
(define (f3 x) x)
(f1 1)
при вызове f1 кладется 1 на стек, потом делается call f1. в f1 вызов f2 хвостовой, по-этому все тело f1 - это просто jmp f2, аналогично с телом f2. внутри них вообще нету никаких ret, хвостовой вызов _заменяет _ ret. А вот в f3 как раз ret уже и будет. То есть здесь один call, один ret, между ними возврат значения из f3, а f1 и f2 - это просто два джампа.

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

Или лучше взять такой код:

можно взять и просто

int f1(int x)
{
	return x <= 1 ? 1 : x * f1(x - 1);
}
всё равно сворачивается до
 8048310:       0f af d0                imul   edx,eax
 8048313:       48                      dec    eax
 8048314:       83 f8 01                cmp    eax,0x1
 8048317:       75 f7                   jne    8048310 <main+0x20>
более сложные случаи тоже удаётся свернуть... СР УВЧ!!

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

можно взять и просто

Нет, нельзя. Речь шла именно о том, как выполняется сама TCO - без инлайнинга и других оптимизаций. И речь не о сворачивании в цикл. Цикла может и не быть вообще.

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

php наверное (:

Нет, в пехепе стек выбивает.

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

можешь попробовать во что скомпилируется что-нибудь типа int f1(int x){return f2(x)}, где f2 - указатель на функцию (чтобы ее не заинлайнило).

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

А теперь ответь мне, чем в бинарнике отличается «положить в регистр инт»

например

80482fb:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
(int у меня тут 32х битный DWORD PTR)

от «положить в регистр флоат».

 80485e3:       db 44 24 1c             fild   DWORD PTR [esp+0x1c]

тут правда в 80и битный FP-регистр ST(0) кладётся DWORD.

а вообще - подучи матчасть.

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

Нет, нельзя. Речь шла именно о том, как выполняется сама TCO - без инлайнинга и других оптимизаций. И речь не о сворачивании в цикл. Цикла может и не быть вообще.

дык тут как свернёшь ХР? Сначала тебе надо сделать из f(x-1) ХР, для чего тебе необходимо разинлайтить x*, развернуть её как TCO, и _после_ этого ты сможешь развернуть TCO f(x-1), которая ДО того не развернётся, ибо не хвостовая. А цикл тут да, не причём.

  • однако может оно и не так работает, ибо если добавить double (именно double, а не целую!) переменную, то сворачивание ломается.
    int f1(int x)
    {
    	double y = x*x;
    	return x <= 1 ? 1 : y + x * f1(x-1);
    }
    
    Похоже причина в том, что засунуть целое в FPU получается только через стек, что ломает нафиг эту вашу TCO (без double оно всё в регистрах сохраняет, стек не нужен, и TCO работает)
drBatty ★★
()
Ответ на: комментарий от drBatty

тут правда в 80и битный FP-регистр ST(0) кладётся DWORD.

А при чем тут 80-битный fp-регистр? Положи флоат в тот же eax, куда инт клал до этого. А так у тебя код отличается только лишь из-за того, что ты кладешь одно и то же значение в разные регистры.

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

Похоже причина в том, что засунуть целое в FPU получается только через стек, что ломает нафиг эту вашу TCO

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

Похоже причина в том, что засунуть целое в FPU получается только через стек, что ломает нафиг эту вашу TCO (без double оно всё в регистрах сохраняет, стек не нужен, и TCO работает)

ТСО никак не работает с регистрами, она работает со стеком и только со стеком. Если твой цикл свернут в регистры - это не ТСО.

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

что-нибудь типа int f1(int x){return f2(x)}, где f2 - указатель на функцию (чтобы ее не заинлайнило).

оно всё равно инлайтит.

typedef int (*ptr_f)(int);

int f4(int x)
{
	return x*x + x;
}


ptr_f f2;

int f3(int x)
{
	f2 = f4;
	return f2(x);
}
 80482f9:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
 80482fc:       c7 05 3c 9b 04 08 50    mov    DWORD PTR ds:0x8049b3c,0x8048450
 8048303:       84 04 08 
 8048306:       8d 50 01                lea    edx,[eax+0x1]
 8048309:       0f af c2                imul   eax,edx
а вот так не инлайтит:
typedef int (*ptr_f)(int);

int f4(int x)
{
	return x*x + x;
}

int f5(int x)
{
	return x*x;
}

ptr_f f2;

int f3(int x)
{
	f2 = x ? f4 : f5;
	return f2(x);
}
 80482f9:       8b 55 08                mov    edx,DWORD PTR [ebp+0x8]
 80482fc:       85 d2                   test   edx,edx
 80482fe:       75 23                   jne    8048323 <main+0x33>
 8048300:       b8 70 84 04 08          mov    eax,0x8048470
 8048305:       a3 8c 9b 04 08          mov    ds:0x8049b8c,eax
 804830a:       89 14 24                mov    DWORD PTR [esp],edx
 804830d:       ff d0                   call   eax ;вызов f4 или f5 в зависимости от x(edx)
 804830f:       89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048313:       c7 04 24 73 88 04 08    mov    DWORD PTR [esp],0x8048873
 804831a:       e8 a1 ff ff ff          call   80482c0 <printf@plt>
 804831f:       31 c0                   xor    eax,eax
 8048321:       c9                      leave  
 8048322:       c3                      ret    
 8048323:       b8 60 84 04 08          mov    eax,0x8048460
 8048328:       eb db                   jmp    8048305 <main+0x15>

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

оно всё равно инлайтит.

Ну ясное дело, он же видит что это константа, вот и инлайнит.

804830d: ff d0 call eax ;вызов f4 или f5 в зависимости от x(edx)

Ну вот, значит TCO в gcc нет. Вопрос закрыт. Если бы было, тут был бы джамп вместо call+ret.

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

А при чем тут 80-битный fp-регистр?

при том, что у меня тут такие double десятибайтовые. Извини, других у мну нет. Скомпиль под любой CPU с SSE2, у тебя будут 64х битные double, а ежели проц поновее, то в одном регистре будут ажно два double сразу. У мну можно загружать и 64х битные, и даже 80и битные, но только из памяти...

Положи флоат в тот же eax, куда инт клал до этого. А так у тебя код отличается только лишь из-за того, что ты кладешь одно и то же значение в разные регистры.

дык правильно - регистр ST(0) нужен для FP, а регистр eax нужен для целых. И сложение в первом будет обработано как FP, а во втором как int. FP через целые не обрабатывают уже лет 30, начиная с i80486, в котором FPU был встроен. С тех пор _все_ данные FP обрабатываются в FPU. Ну а в последние годы уже и в SSE. Ну уж никак не в eax.

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

Ты, дебил, знаешь, какие эвристики кэш в интелах использует? Не знаешь, дебил. А я знаю. Что кэшу данных до фени, через esp адресуют или через что еще. Он даже не знает, откуда абсолютный адрес взялся после MMU.

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

А вот хвостовой рекурсии и лямбд нет.

Как же они тогда работают, если не исполнением машинного кода?

Что не удивительно, так-как отсутствует сама сущность данных - функция.

call/ret, push/pop + стек уже дают некую почву для функций (и делались ради них, но явно не для фортовиков с их парой стеков - вот им приходится извращаться :)).

Например невозможно сделать полноценную передачу даже тривиальной функции типа x+y

Передаём / вызываем по указателю. Если функция known (на уровне исходного кода) - liftим все лямбды и инлайним код.

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

дык правильно - регистр ST(0) нужен для FP, а регистр eax нужен для целых. И сложение в первом будет обработано как FP, а во втором как int. FP через целые не обрабатывают уже лет 30, начиная с i80486, в котором FPU был встроен. С тех пор _все_ данные FP обрабатываются в FPU. Ну а в последние годы уже и в SSE. Ну уж никак не в eax.

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

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

Тут вообще нету никакого TCO. Те оптимизации что делаются тут на интах никакого отношения к TCO не имеют, потому я и сказал, что этот код рассматривать смысла нет.

каким образом можно заинлайтить _рекурсивную_ функцию, да ещё такую, число итераций которой зависит от аргумента? Я знаю только один способ - TCO. Но для этого она должна быть TC. Вот в этом случае gcc сначала делает функцию TC, а потом разворачивает её в jamp (получая цикл в данном случае).

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

ну я посмотрел - не инлайтит, а делает косвенный вызов.

ТСО никак не работает с регистрами, она работает со стеком и только со стеком. Если твой цикл свернут в регистры - это не ТСО.

TCO убирает стек нафиг. Это возможно _только_ если этот стек используется исключительно для TC, в этом, и только в этом случае можно заменить TC на jmp. Если что-то там похимичить со стеком, даже просто через стек запулить в FPU регистр, то всё - TCO ломается. Ну а добиться этого можно только одним способом - ВСЁ хранить в регистрах (в сишечке, в других ЯП можно хранить и в куче).

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

если иф сам хвостовой

Я к тому, что нужно пройтись по цепочке, выяснив что if хвостовой (а вдруг там какой-то сложный вызов?) и тогда «уже» внутренние части считать хвостовыми и так далее.

вобщем-то всегда можно, если нельзя - то он нифига не хвостовой

А рекурсию по бинарному дереву можно считать хвостовой? Или вот как в примере с f_ и f - формально обе ветки хвостовые, но сlang делает TCO для f_ и дальше физически не может ничего сделать с f (то есть - он превратил её в цикл, начал его крутить, а f_ сделала ret n! для n <= 5), gcc просто инлайнит f_ и делает TCO для рекурсивного f (так получается получше).

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

Ну вот, значит TCO в gcc нет. Вопрос закрыт. Если бы было, тут был бы джамп вместо call+ret.

посмотри внимательно на код - это код main(), в которую заинлайтена f3, в которой есть TCO. Но TCO невозможно в main(), т.е. это (в main()) не TC, а в f3 невозможно потому-что эта f3 заинлайтина

А TCO таки есть - смотри, я запретил инлайтить f3, и получил:

08048470 <f3>:
 8048470:       8b 54 24 04             mov    edx,DWORD PTR [esp+0x4]
 8048474:       85 d2                   test   edx,edx
 8048476:       75 18                   jne    8048490 <f3+0x20>
 8048478:       b8 60 84 04 08          mov    eax,0x8048460
 804847d:       a3 7c 9b 04 08          mov    ds:0x8049b7c,eax
 8048482:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 8048486:       ff e0                   jmp    eax
 8048488:       90                      nop
 8048489:       8d b4 26 00 00 00 00    lea    esi,[esi+eiz*1+0x0]
 8048490:       b8 50 84 04 08          mov    eax,0x8048450
 8048495:       a3 7c 9b 04 08          mov    ds:0x8049b7c,eax
 804849a:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
 804849e:       ff e0                   jmp    eax
как видишь, в данном случае ты получил свой jmp eax

ты этого хотел?

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

По крайней мере все с нативным call/cc?

Не знаю, я просто не видел ничего что бы не использовало обычный стек и не падало в SO в определённых случаях.

Вот тут вообще говорится наоборот про «кучу» на стеке.

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

Ты, дебил, знаешь, какие эвристики кэш в интелах использует? Не знаешь, дебил. А я знаю. Что кэшу данных до фени, через esp адресуют или через что еще. Он даже не знает, откуда абсолютный адрес взялся после MMU.

я конечно дебил, но не идиот. Вот код №1

void f(void)
{
    int x;
}

вот №2

void f(void)
{
    int *x = malloc(sizeof(int));
}
Вот в первом случае, я _точно_ знаю, что int x у мну лежит в кеше первого уровня, ведь я прошлой командой вызвал функцию f(), которая записала в стек адрес возврата. А вот где выделила мне память malloc(2) - я в душе ниибу, ибо хрен знает где. Скорее всего - нигде, где-то в VIRT, т.е. и памяти-то такой не существует, её надо сначала выделить, а потом протащить через кеши ВСЕХ уровней.

Теперь разъясни мне, дебилу, в чём моя ошибка?

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

Как же они тогда работают, если не исполнением машинного кода?

понятно как - костылями. А именно указателями на код. См. выше пример с call eax и jmp eax.

call/ret, push/pop + стек уже дают некую почву для функций (и делались ради них, но явно не для фортовиков с их парой стеков - вот им приходится извращаться :)).

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

Передаём / вызываем по указателю. Если функция known (на уровне исходного кода) - liftим все лямбды и инлайним код.

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

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

можно взять и просто

Это рекурсия, а фишка в том, что хвостовой не рекурсивный вызов «в сишке» в jmp тоже превращается (не скажу насколько часто, но иногда замечаю такое у gcc/clang).

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

Да, ты на редкость невменяемый отморозок! Ты x записываешь сначала, нет? Тебе ж нах ненужно его значение по умолчанию (то есть, совершенно случайное число)? Кэш, скорее всего, с write-through семантикой, если это не совсем уж экзотичная ембеддщина.

Так что, твоя ошибка, дебил, в том, что поведение «movl откуда, индекс(%регистр)» для кэша от имени регистра никак не зависит. Можешь теперь рыдать и биться кочаном об стену.

И еще, лошара, с тебя бенчмарк - показывай код, где доступ к массиву, выделенному malloc-ом был бы медленнее, чем доступ к статическому массиву на стеке (или выделенному через alloca).

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

дык правильно - регистр ST(0) нужен для FP, а регистр eax нужен для целых. И сложение в первом будет обработано как FP, а во втором как int. FP через целые не обрабатывают уже лет 30, начиная с i80486, в котором FPU был встроен. С тех пор _все_ данные FP обрабатываются в FPU. Ну а в последние годы уже и в SSE. Ну уж никак не в eax.

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

ты действительно так туп, или слил, но не желаешь в этом признаться?

С т.з. CPU есть

  • целые числа. Они хранятся в памяти для 32х битного CPU в РОН, или в памяти, и берутся по DWORD PTR. Размер определён префиксом, а знаковость/беззнаковость важна только для команд умножения/деления, потому только для них и задаётся (команда сложения даёт верный результат и для знакового и для беззнакового целого)
  • числа с плавающей точкой лежат в стеке FPU, или для более нового CPU в регистрах SSE2+. Есть специальные команды для работы с ними
  • есть указатели, и специальные команды для работы с ними (работа со стеком, а также работы с массивом в стиле C)
  • есть две специальные команды для работы с указателем на код(call/jmp).
drBatty ★★
()
Ответ на: комментарий от drBatty

каким образом можно заинлайтить _рекурсивную_ функцию, да ещё такую, число итераций которой зависит от аргумента?

прикинь, точно также как и нерекурсивную.

Я знаю только один способ - TCO.

ТСО не имеет никакого отношения к инлайнингу.

Вот в этом случае gcc сначала делает функцию TC, а потом разворачивает её в jamp (получая цикл в данном случае).

ГЦЦ там ничего не делает. Как был обычный вызов (не ТС) так он и остался.

ну я посмотрел - не инлайтит, а делает косвенный вызов.

ТСО не инлайнинг и не обычный вызов. Это ТСО. Замена пары call+ret (которые стоят вместе при хвостовом вызове) на один единственный джамп.

TCO убирает стек нафиг.

ТСО не может убирать стек, т.к. она его использует. Она просто не дает стеку расти.

Это возможно _только_ если этот стек используется исключительно для TC, в этом, и только в этом случае можно заменить TC на jmp.

Нет, во всех.

Если что-то там похимичить со стеком, даже просто через стек запулить в FPU регистр, то всё - TCO ломается.

нет, с этим никаких проблем.

Ну а добиться этого можно только одним способом - ВСЁ хранить в регистрах (в сишечке, в других ЯП можно хранить и в куче).

Нет. Повторяю - в ТСО вообще предполагается что регистров у процессора нет. Оно рабоатет толкьо со стеком.

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

Если stackless педон считать нормальным, то есть.

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

как видишь, в данном случае ты получил свой jmp eax

Ну да, теперь все правильно.

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

Не знаю, я просто не видел ничего что бы не использовало обычный стек и не падало в SO в определённых случаях.

с нативной реализацией call/cc приходится «переключаться» между стеками. Соответственно, использовать обычный аппаратный стек нельзя.

Вот тут вообще говорится наоборот про «кучу» на стеке.

Там ненативно, как я понял, через CPS.

Попробуй в Racket, например, там нативные продолжения.

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

понятно как - костылями. А именно указателями на код.

Ну да. Так и запишем - в микроархитектуру зачем-то понапихали ненужных костылей :)

Хотя, где-то ссылка была - вот. Тут тоже можно готовить по-разному :)

Потому, для тривиальных функций это НЕ работает, из-за очень большого оверхеда.

Тривиальные надо инлайнить, само собой.

в том-то и дело, что пользы от лямбды никакой, если код known

А польза от inline и шаблонных функций есть? Они тоже known.

С тем же успехом ты можешь юзать switch/case, который в сишечке отлично работает.

То есть switch/case вместо лямбд и ФВП? Это как? Ну и switch тоже не first class на уровне ISA, почему сразу не pattern-matching, он же, теоретически, zero overhead с точки зрения реализации?

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

С т.з. CPU есть

Дан адрес памяти, как CPU отличить, лежит там целое число, число с плавающей точкой или указатель? То есть если для CPU есть разница, значит он отличить определенно _может_. Как?

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

Это рекурсия, а фишка в том, что хвостовой не рекурсивный вызов «в сишке» в jmp тоже превращается (не скажу насколько часто, но иногда замечаю такое у gcc/clang).

пример см. выше - для выполнения необходимо два условия:

1. вызывающая функция НЕ инлайнится

2. вызываемая функция косвенная

тогда call eax компилятор заменяет на jmp eax. И в чём профит? Да практически 0.

Выигрыш будет тогда, и только тогда, когда вызываемая функция РЕКУРСИВНАЯ. В этом случае получается не просто jmp, а jmp на саму функцию, что эквивалентно while(true){}, т.к. IRL смысл имеет только условный переход, то ХР превращается в цикл. Но ТОЛЬКО ХР.

Возможно компилятор сможет распарсить

void f1()
{
    f2();
}
void f2()
{
    f1();
}
но это вряд-ли. Другие варианты невозможны, ибо прыгать из вызываемой функции в вызывающую можно только вызовом вызывающей.

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

Пока не хотят вводить из-за каких то проблем с безопасностью. Видимо, контекст безопасности будет теряться в JVM при TCO, или что-то вроде того. Кстати, в .NET далеко не сразу научились делать TCO, и я не уверен, что научились делать до конца. То есть, твердых гарантий нет, но, скорее всего, последние версии виртуальной машины .NET, особенно для x64, правильно обработают инструкцию .tail, окажись это действительно хвостовым вызовом. А так, такая инструкция может быть поставлены от балды, и там необязательно должен быть настоящий хвостовой вызов. Короче, тема очень сложная.

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

x86 - CISC, у него до сраки вариантов load/store. Практически каждая инструкция может обратиться к памяти, и, естественно, разные инструкции будут лезть за данными разных типов. Отсюда и весь этот бред с alignment.

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

Выигрыш будет тогда, и только тогда, когда вызываемая функция РЕКУРСИВНАЯ.

Идиот. Тебя уже быдлячьим твоим рыльцем ткнули в functional reactive programmin (которое к функциональщине на самом деле никакого отношения не имеет).

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

Дебил. Не надо косвенного вызова. Сюда смотри, дебил:

extern int g(int x);

int f(int x) {
  int y = x*100;
  return g(y);
}

gcc-4.7 -O3 дает такое:

	.globl _f
_f:
LFB0:
	movl	$100, %eax
	imull	%eax, %edi
	jmp	_g
anonymous
()
Ответ на: комментарий от dave

В общем, если хочется настоящего высокоуровневого ЯП с поддержкой всех плюшек ФП - то про JVM (scala, clojure, kotlin) пока можно забыть?

пора начинать учить haskell :)

p.s. заказал себе недавно книжку про common lisp, ту самую которую перевели недавно. haskell в последующих планах.

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

можешь попробовать во что скомпилируется что-нибудь типа int f1(int x){return f2(x)}, где f2 - указатель на функцию (чтобы ее не заинлайнило).

int f2(int);
int f1(int x) { return f2(x); }

gcc (> O1) и clang (> O0):

f1:
        jmp     f2
int (*f2)(int);
int f1(int x) { return f2(x); }

gcc (> O1):

f1:
        movq    f2(%rip), %rax
        jmp     *%rax

clang (> O0):

f1:                                     # @f1
        jmpq    *f2(%rip)  # TAILCALL
quasimoto ★★★★
()
Ответ на: комментарий от drBatty

что эквивалентно while(true){}

Скорее start: ... return ... ; goto start;

И да

while(true){}

/undecidable ;)

Возможно компилятор сможет распарсить

f1:
.L2:
        jmp     .L2
f2:
.L4:
        jmp     .L4
quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

У моего clangа баг:

$ cat g.c
#include <stdio.h>
void f2();
void f1() { f2(); }
void f2() { f1(); }
int main() { f1(); printf("decidable\n"); }
$ clang -O0 g.c -o g && ./g
[1]    29099 segmentation fault (core dumped)  ./g
$ clang -O1 g.c -o g && ./g
decidable
$ clang -O2 g.c -o g && ./g
decidable
$ clang -O3 g.c -o g && ./g
decidable
quasimoto ★★★★
()
Ответ на: комментарий от drBatty

тогда call eax компилятор заменяет на jmp eax. И в чём профит? Да практически 0.

И еще убирается ret. Ну если ф-и мелкие, то call+ret существенно замедлят выполнение по сравнению с обычным джампом. А вообще профит в том, что в этом случае не растет стек.

Выигрыш будет тогда, и только тогда, когда вызываемая функция РЕКУРСИВНАЯ

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

Возможно компилятор сможет распарсить ... но это вряд-ли.

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

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

x86 - CISC, у него до сраки вариантов load/store. Практически каждая инструкция может обратиться к памяти, и, естественно, разные инструкции будут лезть за данными разных типов. Отсюда и весь этот бред с alignment.

Это все ясно, но речь то о том, что у процессора нету никакого способа определить, данные какого типа лежат в памяти. Так что никто не мешает обрабатывать данные одного типа командой для данных другого. Потому что процессор ничего не знает о типах. Грубо говоря тип есть только один - машинное слово. И процессор умеет работать только с машинными словами - больше ни с чем.

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

Ну, скажем, scala, а особенно clojure, вполне ФП, но осадок есть. А так, да, haskell гораздо ближе к золотой мечте ФП. Что касается common lisp, то если раньше лет тридцать назад, почти все кричали, что это чуть ли не главный язык ФП, то теперь его многие низвели до ранга «императивного» языка программирования. Но по мне common lisp вполне функционален. Сейчас haskell и common lisp - два антипода и, пожалуй, два самых глубоких языка, не рассматривая экзотику, конечно.

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

что тебя так веселит? идеально сбалансированных деревьев на практике почти не встречается. учи матчасть.

меня веселит, что дерево может быть вовсе не сбалансированным

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

скажем, у scala и clojure есть вполне своя ниша. Где, например, много кода на Java, богатая и функциональная java-библиотека, и ею надо пользоваться, а clojure это упрощает, «ускоряет», так сказать, процесс разработки.

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

это не оправдание того, что нужно делать двойную работу - сначала заворачивать всё в функции, а потом радоваться, что компилятор что-то развернул. Если в твоём ЯП нет тех же циклов - это проблема только твоего ЯП, ибо в процессорах они есть. И код разворачивается для процессора. А не для тебя.

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

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

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

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

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

Согласен, но проблема в том, что явовские библиотеки часто написаны не в стиле ФП, поэтому не всегда видны преимущества новых языков. А что касается Scala, то на мой взгляд она и не очень-то функциональная, т.е. точно не больше, чем common lisp, но Scala выручает хорошая и богатая библиотека коллекций с упором на иммутабельность, но потроха этой библиотеки - такая жуть!

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