Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 07:23
Оценка: 66 (3)
Референтный код на C#:

  Скрытый текст
using System;
using System.Diagnostics;

namespace TestTailCall
{
    class Program
    {
        static void Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();
            fun1(0);
            sw.Stop();
            Console.WriteLine(sw.Elapsed);
            Console.ReadLine();
        }
        
        static int fun1(int count)
        {
            if (count < 10000)
            {
                count = count + 1;
                return fun2(count);
            }
            else
                return count;
        }


        static int fun2(int count)
        {
            if (count < 10000)
            {
                count = count + 1;
                return fun1(count);
            }
            else
                return count;
        }
    }
}


Версия на MSIL (писал в соответствии с описанием Tailcall в MSDN, исходя из того, что для корректной обработки хвостового вызова обязательно должна присутствовать цепочка tail. -> call -> ret):

  Скрытый текст
.assembly TestTailCall
{
}

.namespace TestTailCall
{
    .class private auto ansi beforefieldinit Program
    {
        .method public hidebysig static void Main(string[] args) cil managed
        {
            .entrypoint
            .maxstack 1
                .locals init (
                [0] class [System]System.Diagnostics.Stopwatch)
            newobj instance void [System]System.Diagnostics.Stopwatch::.ctor()
            stloc.0 
            ldloc.0 
            callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
            ldc.i4.0 
            call int32 TestTailCall.Program::fun1(int32)
            pop 
            ldloc.0 
            callvirt instance void [System]System.Diagnostics.Stopwatch::Stop()
            ldloc.0 
            callvirt instance valuetype [mscorlib]System.TimeSpan [System]System.Diagnostics.Stopwatch::get_Elapsed()
            box [mscorlib]System.TimeSpan
            call void [mscorlib]System.Console::WriteLine(object)
            call string [mscorlib]System.Console::ReadLine()
            pop
            ret 
        }
        
        .method private hidebysig static int32 fun1(int32 count) cil managed
        {
            .maxstack 2
            .locals init (
                [0] int32)
                
            ldc.i4 10000
            ldarg.0
            clt
            brtrue.s L1
            
            ldarg.0
            ldc.i4.1
            add
            stloc.0
            ldloc.0
            
            tail.
            call int32 TestTailCall.Program::fun2(int32)
            ret
            L1: ldarg.0
            ret
        }
        
        
        .method private hidebysig static int32 fun2(int32 count) cil managed
        {
            .maxstack 2
            .locals init (
                [0] int32)
                
            ldc.i4 10000
            ldarg.0
            clt
            brtrue.s L1
            
            ldarg.0
            ldc.i4.1
            add
            stloc.0
            ldloc.0
            
            tail.
            call int32 TestTailCall.Program::fun1(int32)
            ret
            L1: ldarg.0
            ret
        }
    }
}


Для отключения Tailcall достаточно закомментировать строчку с tail.

Итак, результаты тестов для счетчика 10000:

Tailcall: .0004292
Без Tailcall: .0008141


Для счетчика 100000:

Tailcall: .0008388
Без Tailcall: срыв стека


Выводы делайте сами.
Re: Бенчмарк: call версус tail. cal
От: hardcase Пират http://nemerle.org
Дата: 27.05.11 07:53
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

Для взаимнорекурсивных функций вполне годится. А для случая с одной функцией br и замена параметров — наше все.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[2]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 08:07
Оценка: -1
Здравствуйте, hardcase, Вы писали:

H>Для взаимнорекурсивных функций вполне годится. А для случая с одной функцией br и замена параметров — наше все.


Да. Это, собственно, две разные оптимизации по сути — раскрутка хвостовой рекурсии в цикл и использование префикса tail. для хвостовых вызовов. Собственно, о чем я как раз и хотел сказать. А тот тут ваше "руководство" почему-то считает, что использование tail. вообще не имеет смысла.

Кстати, я время криво померил. У меня в итоговое время попадает так же и Джит. Так что фактически там разница даже поболее двух раз будет.
Re[3]: Бенчмарк: call версус tail. cal
От: hardcase Пират http://nemerle.org
Дата: 27.05.11 08:14
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Да. Это, собственно, две разные оптимизации по сути — раскрутка хвостовой рекурсии в цикл и использование префикса tail. для хвостовых вызовов. Собственно, о чем я как раз и хотел сказать. А тот тут ваше "руководство" почему-то считает, что использование tail. вообще не имеет смысла.


Что-то мне подсказывает, что "руководство" имеет в виду использование .tail в рамках одной функции.
/* иЗвиНите зА неРовнЫй поЧерК */
Re: Бенчмарк: call версус tail. cal
От: catbert  
Дата: 27.05.11 08:14
Оценка: +1
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Выводы делайте сами.


Версия фреймворка? Машина?
Re[2]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 08:21
Оценка:
Здравствуйте, catbert, Вы писали:

C>Версия фреймворка? Машина?


4.0
i7 M 620

Да, и тест не очень корректный, я прокосячил малость — не учитывается время на Джит. Т.е. реальная ситуация должна быть еще более благоприятной для tailcall.
Re[4]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 08:23
Оценка:
Здравствуйте, hardcase, Вы писали:

ВВ>>Да. Это, собственно, две разные оптимизации по сути — раскрутка хвостовой рекурсии в цикл и использование префикса tail. для хвостовых вызовов. Собственно, о чем я как раз и хотел сказать. А тот тут ваше "руководство" почему-то считает, что использование tail. вообще не имеет смысла.

H>Что-то мне подсказывает, что "руководство" имеет в виду использование .tail в рамках одной функции.

Честно говоря, не хочется играть в испорченный телефон. Но почему-то мою мысль о том, что tail recursion и proper tail call — это две *разные* оптимизации, не восприняли.
Re: Бенчмарк: call версус tail. cal
От: hardcase Пират http://nemerle.org
Дата: 27.05.11 17:42
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Выводы делайте сами.


Дошел до дома. Проверил. Тормозит. Флаг компилятора -Ot, .net 4.0, x86. Не исключаю возможности, что тормоза есть лишь на x86.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[2]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 19:17
Оценка:
Здравствуйте, hardcase, Вы писали:

H>Здравствуйте, Воронков Василий, Вы писали:

ВВ>>Выводы делайте сами.
H>Дошел до дома. Проверил. Тормозит. Флаг компилятора -Ot, .net 4.0, x86. Не исключаю возможности, что тормоза есть лишь на x86.

Забавно, блин. На x86 действительно картина совершенно иная. Я немного поправил тест — так, чтобы он не учитывал Джит. В начале метода Main добавил:

ldc.i4.0
call int32 TestTailCall.Program::fun1(int32)
pop


Т.о. тест прогоняется два раза и время компиляции не учитывается.

Обновленный тест (i5 750, Win7, 4.0):

x64
tail. call  .0000119
call        .0002948

x86
tail. call  .0002513
call        .0000184


Они практически меняются местами
Что-то они намудрили
Re[3]: Бенчмарк: call версус tail. cal
От: hardcase Пират http://nemerle.org
Дата: 27.05.11 19:24
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Они практически меняются местами

ВВ>Что-то они намудрили

На x64 другой JIT-компилятор и другая конвенция вызовов.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[4]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 19:28
Оценка: +1
Здравствуйте, hardcase, Вы писали:

ВВ>>Они практически меняются местами

ВВ>>Что-то они намудрили
H>На x64 другой JIT-компилятор и другая конвенция вызовов.

Я понимаю, что другой. Но "tail." сам по себе ничего не меняет. По большому счету он должен просто снимать со стека текущий фрейм. Здесь же речь идет о разнице в производительности в два порядка. Это при том, что далее идет самый обычный call, который в свою очередь на два порядка быстрее. Я находить это странным.
Re[5]: Бенчмарк: call версус tail. cal
От: hardcase Пират http://nemerle.org
Дата: 27.05.11 19:38
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Я находить это странным.


Где-то об этом писалось. В x86 какая-то функция вызывается вместо джита её тела по месту.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[6]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 19:57
Оценка:
Здравствуйте, hardcase, Вы писали:

H>Здравствуйте, Воронков Василий, Вы писали:

ВВ>>Я находить это странным.
H>Где-то об этом писалось. В x86 какая-то функция вызывается вместо джита её тела по месту.

Без tail.:

  Скрытый текст
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        eax  
00000004  mov         eax,ecx 
00000006  cmp         eax,2710h 
0000000b  jg          00000023 
0000000d  inc         eax  
0000000e  mov         dword ptr [ebp-4],eax 
00000011  call        73B7D540 
00000016  mov         ecx,dword ptr [ebp-4] 
00000019  call        dword ptr ds:[00153014h] 
0000001f  mov         esp,ebp 
00000021  pop         ebp  
00000022  ret              
00000023  mov         esp,ebp 
00000025  pop         ebp  
00000026  ret


С tail.:

  Скрытый текст
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  
00000006  push        eax  
00000007  mov         eax,ecx 
00000009  cmp         eax,2710h 
0000000e  jg          0000003E 
00000010  inc         eax  
00000011  mov         dword ptr [ebp-10h],eax 
00000014  call        7381D540 
00000019  mov         ecx,dword ptr [ebp-10h] 
0000001c  mov         eax,dword ptr ds:[00283014h] 
00000022  push        0    
00000024  push        0    
00000026  push        1    
00000028  push        eax  
00000029  cmp         dword ptr ds:[74664394h],0 
00000030  je          00000039 
00000032  push        ecx  
00000033  call        73E4D77B 
00000038  pop         ecx  
00000039  call        73BF27C0 
0000003e  pop         ecx  
0000003f  pop         ebx  
00000040  pop         esi  
00000041  pop         edi  
00000042  pop         ebp 
00000043  ret


Видно, что с tail. действительно есть какой-то левый вызов.
Re[3]: Бенчмарк: call версус tail. cal
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.05.11 20:25
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Они практически меняются местами

ВВ>Что-то они намудрили

Разные команды делали. Для х64 делали Вижуал Сишники, а для х86 дотнетчики (если я не ошибаюсь).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: Бенчмарк: call версус tail. cal
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.05.11 20:27
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>
ВВ>x64
ВВ>tail. call  .0000119
ВВ>call        .0002948

ВВ>x86
ВВ>tail. call  .0002513
ВВ>call        .0000184
ВВ>


ВВ>Они практически меняются местами

ВВ>Что-то они намудрили

Ну, так теперь то ты понимашь, что это косяк? Могли бы везде сделать шустро. Переписывание в цикл всегда работает шустро.

Кстати, попробуй его тоже в тесте реализовать. Чтобы было с чем сравнивать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[7]: Бенчмарк: call версус tail. cal
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.05.11 20:28
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Видно, что с tail. действительно есть какой-то левый вызов.


Как ассемблер добывал?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[8]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 20:36
Оценка:
Здравствуйте, VladD2,

ВВ>>Видно, что с tail. действительно есть какой-то левый вызов.

VD>Как ассемблер добывал?

Аттач к процессу, уже запущенному.
Re[4]: Бенчмарк: call версус tail. cal
От: Воронков Василий Россия  
Дата: 27.05.11 20:45
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Ну, так теперь то ты понимашь, что это косяк? Могли бы везде сделать шустро. Переписывание в цикл всегда работает шустро.


Для х86 — косяк. Хотя однозначного ответа об использовании tail call эти тесты все же не дают. На х64 быстро, под моно, как вы говорите, тоже быстро. Ну и все-таки стек не слетает.

VD>Кстати попробуй его тоже в тесте реализовать. Чтобы было с чем сравнивать.


Да с циклом и так все понятно. Меня вообще proper tail call интересует в первую очередь не в связи с рекурсией, а в связи с CPS, для которого он есть первое и необходимое требование.
Re[9]: Бенчмарк: call версус tail. cal
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.05.11 21:04
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Аттач к процессу, уже запущенному.


Релизному?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Бенчмарк: call версус tail. cal
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.05.11 21:06
Оценка:
Здравствуйте, Воронков Василий, Вы писали:

ВВ>Для х86 — косяк. Хотя однозначного ответа об использовании tail call эти тесты все же не дают. На х64 быстро, под моно, как вы говорите, тоже быстро. Ну и все-таки стек не слетает.


Со стеком все ясно. Только на практике он и так не слетает. Ни одно жалобы за много лет.

Что до tail call, то опция компиляции такая есть. Кому оно надо, может включить.

VD>>Кстати попробуй его тоже в тесте реализовать. Чтобы было с чем сравнивать.


ВВ>Да с циклом и так все понятно. Меня вообще proper tail call интересует в первую очередь не в связи с рекурсией, а в связи с CPS, для которого он есть первое и необходимое требование.


Не, не. Понятно не все. Ты сделай и покажи результат. Потому как может оказаться, что по сравнению с ним tail call не конкурент.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.