Информация об изменениях

Сообщение Re[11]: Минутка WTF-19: Catch me if you can от 20.03.2017 19:05

Изменено 20.03.2017 19:10 vdimas

Re[11]: Минутка WTF-19: Catch me if you can
Здравствуйте, Sinix, Вы писали:

S>Мы как-то по кругу ходим. Я выше уже дал примеров 5 реальных багов, связанных с использованием ImmutableArray<T>. Но ты продолжаешь упорно доказывать, что тип ок, это девелоперы неправильные. Если это твоя принципиальная позиция — ок, закругляемся.


Не надо наговаривать. Я говорил, что девелоперы пытались использовать правильные практики.
Только опыта еще маловато, вестимо.


S>Эмм, ты описываешь worst practices и на этой основе доказываешь, что best не нужны? * чешет в затылке.


А судьи кто? ))

Твои best practices для дотнеты были разработаны с прицелом, что архитектура строится на ref-типах, а value-типы используются сугубо как "плоские данные". Но это уровень ноль, а не "best practices". По современным меркам это равносильно отсутствию всяких практик.


S>Бинго. Только перед этим готовые рекомендации надо знать. Досконально. Чтобы не тратить время на детские ошибки и не вставать в позицию "я на эти грабли ещё не наступал => все, кто меня предупреждают == нубы". Вот как тут:


Рекомендации писали на "отгребись", сорри.
Эти рекомендации не полны, не покрывают целого пласта востребованных кейзов.

Я же тебе писал:

Гадлайнам надо не только следовать, их надо активно разрабатывать в процессе своей деятельности. Собсно, они сами должны оседать как "побочный эффект вычислений".


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


S>Кто-то что-то мерял не так. Скорее всего, ещё и под старым x86 JIT-ом. C RyuJit не будет там разов, если дело именно в косвенных вызовах. 5..10 процентов — верю, больше — нет.


Вот прям вот так? ))
И прям ни капли сомнений?
Т.е., тебя тоже как инженера уже можно начинать хоронить?


S>В чём разница между RefWrapper и Lizt<T> — посмотри сам. Сам напросился — нефиг рассказывать про перфоманс с позиции "мне _кажется_, что причина в косвенных вызовах"


Я не говорил "мне кажется", я говорил "есть некие тесты с конкретными результатами".
В общем, нашел я тот топик:
http://rsdn.org/forum/dotnet/4295992.1
Автор: vdimas
Дата: 01.06.11


Проверил только что на дотнете 4.6.1, получил разницу в 2.3 раза и в 1.8 раз на x86 и x64 соответственно.


S>
  пруфкод

Ох, и тебя самого ничего не настрожило разве? ))
А где же сомнения?
Без сомнений ведь не бывает здравого смысла.

Кароч, я добавил к длине массивов 3 нуля в твоём тесте и погонял.
Результаты для x86 :
            Method |          Mean |    StdDev | Scaled | Scaled-StdDev |
------------------ |-------------- |---------- |------- |-------------- |
    TestRefWrapper | 2,345.5918 us | 7.4546 us |   1.00 |          0.00 |
 TestStructWrapper |   961.5590 us | 6.0217 us |   0.41 |          0.00 |
          TestList | 1,499.5782 us | 4.4925 us |   0.64 |          0.00 |
     TestListReset | 1,497.4722 us | 4.1352 us |   0.64 |          0.00 |
         TestArray |   960.5809 us | 2.4241 us |   0.41 |          0.00 |


Результаты для x64 :
            Method |          Mean |    StdDev | Scaled | Scaled-StdDev |
------------------ |-------------- |---------- |------- |-------------- |
    TestRefWrapper |   816.8624 us | 3.9044 us |   1.00 |          0.00 |
 TestStructWrapper |   774.8507 us | 1.6223 us |   0.95 |          0.00 |
          TestList |   996.5568 us | 3.0444 us |   1.22 |          0.01 |
     TestListReset | 1,005.6156 us | 4.9246 us |   1.23 |          0.01 |
         TestArray |   775.9587 us | 3.2324 us |   0.95 |          0.01 |


Лично мне тут сходу всё стало ясно, бо чудес, увы, не бывает.
Например, в дебаге разница по-прежнему заметная.
Кароч, преодолевая свою лень, поясняю происходящее:

* Для простоты дизассемблированного кода сделал все поля статическими.

* Запустил релизный вариант как отдельный процесс, вставил в нужном месте ожидание с клавиатуры и присоединился отладчиком.

* Смотрим на дизассемблированный код:
public long TestRefWrapper()
...
            var result = 0L;
00007FFCF26D06C0  sub         rsp,28h  
00007FFCF26D06C4  xor         eax,eax  
            var local = _ref;
00007FFCF26D06C6  mov         rdx,24E100059A0h  
00007FFCF26D06D0  mov         rdx,qword ptr [rdx]  
            for (int i = 0, e = local.Length; i < e; i++)
00007FFCF26D06D3  xor         ecx,ecx  
00007FFCF26D06D5  mov         rdx,qword ptr [rdx+8]  
00007FFCF26D06D9  mov         r8d,dword ptr [rdx+8]  
00007FFCF26D06DD  mov         r9d,r8d  
00007FFCF26D06E0  test        r9d,r9d  
00007FFCF26D06E3  jle         00007FFCF26D0702  
00007FFCF26D06E5  mov         r10,rdx  
00007FFCF26D06E8  cmp         ecx,r8d  
00007FFCF26D06EB  jae         00007FFCF26D0707  
00007FFCF26D06ED  movsxd      r11,ecx  
00007FFCF26D06F0  mov         r10d,dword ptr [r10+r11*4+10h]  
00007FFCF26D06F5  movsxd      r10,r10d  
00007FFCF26D06F8  add         rax,r10  
00007FFCF26D06FB  inc         ecx  
00007FFCF26D06FD  cmp         ecx,r9d  
00007FFCF26D0700  jl          00007FFCF26D06E5


* Обращаем внимание на кеширование в регистрах внутренних полей объектов по ссылке (чего-чего???):
mov rdx,24E100059A0h      // взяли адрес статического объекта - ссылки _ref
mov rdx,qword ptr [rdx]   // загрузили значение по ссылке - это адрес объекта RefWrapper.
mov rdx,qword ptr [rdx+8] // сразу же прочитали первое поле объекта и запомнили его в rdx



Итого, в rdx у нас лежит указатель на "обернутый" массив, а вовсе не указатель на RefWrapper, который мы наивно собирались протестировать.
Далее в r9d помещается длина этого массива и в цикле происходит сравнение с ней счетчика i, под который используется регистр ecx.

* Итого, получилась профанация, а не тест. Через глобальный анализ джит (или кто там у них) "догадался", что обращения к полю _ref вне метода TestRefWrapper не наблюдается, поэтому он смело так предположил, что ему достаточно провести разыменование ссылки лишь в самом начале метода-теста, а в цикле уже не надо.

Но этого мало, смотри на цифры замеров еще раз.
Всего одна лишняя операция разыменования ДО начала основного цикла в тесте сделала результаты TestRefWrapper всё-равно хуже.


S>Остальное поскипал, т.к. там тоже надо сначала матчасть объяснять, а мне это уже влом.


Пару раз написал и стёр...
По грани ходишь, смотрю...

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

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

Я с такими вещами сталкиваюсь ежедневно, поэтому результатам твоего "теста" не удивился ни разу, ес-но.

ИМХО, тут надо не "матчасть глупому сишнику объяснять" (С), а попробовать найти рекомендации для подобных практик.
Спустись с небес, кароч, а то воспарил, смотрю. ))

=====================
Итак, анализируем проблему:

  • раз неявный боксинг является источником проблем, то предлагаю НЕ реализовывать в обертках навроде ImmutableArray интерфейсы IEnumerable и Ко. Это будет защита на уровне системы типов.

  • Далее можно попытаться придумать какую-нить общую практику для таких оберток, выраженную в виде хелпера, например так:
    interface IObjectProxy {
        bool IsEmpty { get; }
    }
    
    interface IEnumerableProxy<T> : IObjectProxy
    {
       IEnumerable<T> ToEnumerable();
       IEnumerator<T> GetEnumerator();
    }
    
    interface ICollectionProxy<T> : IEnumerableProxy<T>
    {
       ICollection<T> ToCollection();
    }
    
    interface IListProxy<T> : ICollectionProxy<T>
    {
       ICollection<T> ToCollection();
    }


  • Соответственно, методы-расширения в моём пруфе можно допилить до такого:
        private static void CheckNotNull<T>(ref T proxy) where T : IObjectProxy 
        {
            if (proxy.IsEmpty)
                throw new NullReferenceException();
        }
    
        public static int Sum(this T source) where T : IEnumerableProxy<int>
        {
            CheckNotNull(source);
            return Enumerable.Sum(source.ToEnumerable(););
        }

    Итого, уходит зависимость методов-расширений от некоего конкретного типа ImmutableArray<T>.

  • Ну и помним, что методы-хелперы для синтаксиса foreach кушатьинтерфейсов не просят.
    Ву а ля или еще нет?
  • Re[11]: Минутка WTF-19: Catch me if you can
    Здравствуйте, Sinix, Вы писали:

    S>Мы как-то по кругу ходим. Я выше уже дал примеров 5 реальных багов, связанных с использованием ImmutableArray<T>. Но ты продолжаешь упорно доказывать, что тип ок, это девелоперы неправильные. Если это твоя принципиальная позиция — ок, закругляемся.


    Не надо наговаривать. Я говорил, что девелоперы пытались использовать правильные практики.
    Только опыта еще маловато, вестимо.


    S>Эмм, ты описываешь worst practices и на этой основе доказываешь, что best не нужны? * чешет в затылке.


    А судьи кто? ))

    Твои best practices для дотнеты были разработаны с прицелом, что архитектура строится на ref-типах, а value-типы используются сугубо как "плоские данные". Но это уровень ноль, а не "best practices". По современным меркам это равносильно отсутствию всяких практик.


    S>Бинго. Только перед этим готовые рекомендации надо знать. Досконально. Чтобы не тратить время на детские ошибки и не вставать в позицию "я на эти грабли ещё не наступал => все, кто меня предупреждают == нубы". Вот как тут:


    Рекомендации писали на "отгребись", сорри.
    Эти рекомендации не полны, не покрывают целого пласта востребованных кейзов.

    Я же тебе писал:

    Гадлайнам надо не только следовать, их надо активно разрабатывать в процессе своей деятельности. Собсно, они сами должны оседать как "побочный эффект вычислений".


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


    S>Кто-то что-то мерял не так. Скорее всего, ещё и под старым x86 JIT-ом. C RyuJit не будет там разов, если дело именно в косвенных вызовах. 5..10 процентов — верю, больше — нет.


    Вот прям вот так? ))
    И прям ни капли сомнений?
    Т.е., тебя тоже как инженера уже можно начинать хоронить?


    S>В чём разница между RefWrapper и Lizt<T> — посмотри сам. Сам напросился — нефиг рассказывать про перфоманс с позиции "мне _кажется_, что причина в косвенных вызовах"


    Я не говорил "мне кажется", я говорил "есть некие тесты с конкретными результатами".
    В общем, нашел я тот топик:
    http://rsdn.org/forum/dotnet/4295992.1
    Автор: vdimas
    Дата: 01.06.11


    Проверил только что на дотнете 4.6.1, получил разницу в 2.3 раза и в 1.8 раз на x86 и x64 соответственно.


    S>
      пруфкод

    Ох, и тебя самого ничего не настрожило разве? ))
    А где же сомнения?
    Без сомнений ведь не бывает здравого смысла.

    Кароч, я добавил к длине массивов 3 нуля в твоём тесте и погонял.
    Результаты для x86 :
                Method |          Mean |    StdDev | Scaled | Scaled-StdDev |
    ------------------ |-------------- |---------- |------- |-------------- |
        TestRefWrapper | 2,345.5918 us | 7.4546 us |   1.00 |          0.00 |
     TestStructWrapper |   961.5590 us | 6.0217 us |   0.41 |          0.00 |
              TestList | 1,499.5782 us | 4.4925 us |   0.64 |          0.00 |
         TestListReset | 1,497.4722 us | 4.1352 us |   0.64 |          0.00 |
             TestArray |   960.5809 us | 2.4241 us |   0.41 |          0.00 |


    Результаты для x64 :
                Method |          Mean |    StdDev | Scaled | Scaled-StdDev |
    ------------------ |-------------- |---------- |------- |-------------- |
        TestRefWrapper |   816.8624 us | 3.9044 us |   1.00 |          0.00 |
     TestStructWrapper |   774.8507 us | 1.6223 us |   0.95 |          0.00 |
              TestList |   996.5568 us | 3.0444 us |   1.22 |          0.01 |
         TestListReset | 1,005.6156 us | 4.9246 us |   1.23 |          0.01 |
             TestArray |   775.9587 us | 3.2324 us |   0.95 |          0.01 |


    Лично мне тут сходу всё стало ясно, бо чудес, увы, не бывает.
    Например, в дебаге разница по-прежнему заметная.
    Кароч, преодолевая свою лень, поясняю происходящее:

    * Для простоты дизассемблированного кода сделал все поля статическими.

    * Запустил релизный вариант как отдельный процесс, вставил в нужном месте ожидание с клавиатуры и присоединился отладчиком.

    * Смотрим на дизассемблированный код:
    public long TestRefWrapper()
    ...
                var result = 0L;
    00007FFCF26D06C0  sub         rsp,28h  
    00007FFCF26D06C4  xor         eax,eax  
                var local = _ref;
    00007FFCF26D06C6  mov         rdx,24E100059A0h  
    00007FFCF26D06D0  mov         rdx,qword ptr [rdx]  
                for (int i = 0, e = local.Length; i < e; i++)
    00007FFCF26D06D3  xor         ecx,ecx  
    00007FFCF26D06D5  mov         rdx,qword ptr [rdx+8]  
    00007FFCF26D06D9  mov         r8d,dword ptr [rdx+8]  
    00007FFCF26D06DD  mov         r9d,r8d  
    00007FFCF26D06E0  test        r9d,r9d  
    00007FFCF26D06E3  jle         00007FFCF26D0702  
    00007FFCF26D06E5  mov         r10,rdx  
    00007FFCF26D06E8  cmp         ecx,r8d  
    00007FFCF26D06EB  jae         00007FFCF26D0707  
    00007FFCF26D06ED  movsxd      r11,ecx  
    00007FFCF26D06F0  mov         r10d,dword ptr [r10+r11*4+10h]  
    00007FFCF26D06F5  movsxd      r10,r10d  
    00007FFCF26D06F8  add         rax,r10  
    00007FFCF26D06FB  inc         ecx  
    00007FFCF26D06FD  cmp         ecx,r9d  
    00007FFCF26D0700  jl          00007FFCF26D06E5


    * Обращаем внимание на кеширование в регистрах внутренних полей объектов по ссылке (чего-чего???):
    mov rdx,24E100059A0h      // взяли адрес статического объекта - ссылки _ref
    mov rdx,qword ptr [rdx]   // загрузили значение по ссылке - это адрес объекта RefWrapper.
    mov rdx,qword ptr [rdx+8] // сразу же прочитали первое поле объекта и запомнили его в rdx



    Итого, в rdx у нас лежит указатель на "обернутый" массив, а вовсе не указатель на RefWrapper, который мы наивно собирались протестировать.
    Далее в r9d помещается длина этого массива и в цикле происходит сравнение с ней счетчика i, под который используется регистр ecx, т.е. не происходит такого разыменования и при вызове метода Length.

    * Итого, получилась профанация, а не тест. Через глобальный анализ джит (или кто там у них) "догадался", что обращения к полю _ref вне метода TestRefWrapper не наблюдается, поэтому он смело так предположил, что ему достаточно провести разыменование ссылки лишь в самом начале метода-теста, а в цикле уже не надо.

    Но этого мало, смотри на цифры замеров еще раз.
    Всего одна лишняя операция разыменования ДО начала основного цикла в тесте сделала результаты TestRefWrapper всё-равно хуже.


    S>Остальное поскипал, т.к. там тоже надо сначала матчасть объяснять, а мне это уже влом.


    Пару раз написал и стёр...
    По грани ходишь, смотрю...

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

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

    Я с такими вещами сталкиваюсь ежедневно, поэтому результатам твоего "теста" не удивился ни разу, ес-но.

    ИМХО, тут надо не "матчасть глупому сишнику объяснять" (С), а попробовать найти рекомендации для подобных практик.
    Спустись с небес, кароч, а то воспарил, смотрю. ))

    =====================
    Итак, анализируем проблему:

  • раз неявный боксинг является источником проблем, то предлагаю НЕ реализовывать в обертках навроде ImmutableArray интерфейсы IEnumerable и Ко. Это будет защита на уровне системы типов.

  • Далее можно попытаться придумать какую-нить общую практику для таких оберток, выраженную в виде хелпера, например так:
    interface IObjectProxy {
        bool IsEmpty { get; }
    }
    
    interface IEnumerableProxy<T> : IObjectProxy
    {
       IEnumerable<T> ToEnumerable();
       IEnumerator<T> GetEnumerator();
    }
    
    interface ICollectionProxy<T> : IEnumerableProxy<T>
    {
       ICollection<T> ToCollection();
    }
    
    interface IListProxy<T> : ICollectionProxy<T>
    {
       ICollection<T> ToCollection();
    }


  • Соответственно, методы-расширения в моём пруфе можно допилить до такого:
        private static void CheckNotNull<T>(ref T proxy) where T : IObjectProxy 
        {
            if (proxy.IsEmpty)
                throw new NullReferenceException();
        }
    
        public static int Sum(this T source) where T : IEnumerableProxy<int>
        {
            CheckNotNull(source);
            return Enumerable.Sum(source.ToEnumerable(););
        }

    Итого, уходит зависимость методов-расширений от некоего конкретного типа ImmutableArray<T>.

  • Ну и помним, что методы-хелперы для синтаксиса foreach кушатьинтерфейсов не просят.
    Ву а ля или еще нет?