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

Сообщение Re[33]: Carbon от 22.04.2024 20:02

Изменено 22.04.2024 20:07 vdimas

Re[33]: Carbon
Здравствуйте, Sinclair, Вы писали:

CC>>INC EAX выставит SF при знаковом переполнении, так что CMP там в общем то и не нужен.

S>Нужен. Если мы сделаем INC на -8 (0xFFFFFFF8), то получится 0xFFFFFFF9, и SF==1.
S>Прыжки вокруг флагов, инкрементов, и вычитаний нужны только оттого, что компилятор не может статически решить уравнение x+1<x.

Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?

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

В дотнете я как-то показывал трюк эмуляции числовых-константных параметров шаблонов:
https://godbolt.org/z/fKvE14G33
using System;
using System.Runtime.CompilerServices;

interface IYesNo { 
    bool Value { get; }
}

struct Yes : IYesNo {
    public bool Value => true;
}

struct No : IYesNo {
    public bool Value => false;
}

class SomeType<TConfig> where TConfig : struct, IYesNo
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Method() {
        if(default(TConfig).Value) {
            Console.WriteLine("Yes");
        }
        else {
            Console.WriteLine("No");
        }
    }
}

class Program
{
    static void Main()  { 
        SomeType<Yes>.Method();
        SomeType<No>.Method();
    }
}

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

Кстате, в 8-м дотнете в сравнении с 5-6-ми, где я в последний раз проверял этот трюк, наконец-то убрали ненужную двойную инициализацию временного значения default(TConfig), где сгенерённый код выглядел полнейшим нубством.

Вот для 8-го дотнета:
Program:Main() (FullOpts):
       push     rbp
       mov      rbp, rsp
       mov      rdi, 0x7F2C74BD06E8      ; 'Yes'
       call     [System.Console:WriteLine(System.String)]
       mov      rdi, 0x7F2C74BD0708      ; 'No'
       call     [System.Console:WriteLine(System.String)]
       nop      
       pop      rbp
       ret


Вот для 6-го:
Program:Main():
       push     rbp
       sub      rsp, 16
       lea      rbp, [rsp+10H]
       mov      byte  ptr [rbp-08H], 0
       lea      rdi, bword ptr [rbp-08H]
       mov      byte  ptr [rdi], 0
       mov      rdi, qword ptr [(reloc)]
       mov      rdi, gword ptr [rdi]
       call     [System.Console:WriteLine(System.String)]
       mov      byte  ptr [rbp-10H], 0
       lea      rdi, bword ptr [rbp-10H]
       mov      byte  ptr [rdi], 0
       mov      rdi, qword ptr [(reloc)]
       mov      rdi, gword ptr [rdi]
       call     [System.Console:WriteLine(System.String)]
       nop      
       add      rsp, 16
       pop      rbp
       ret


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


S>То есть для signed типов он решение находит (просто неверное с т.з. человеческой логики), а для unsigned — увы.


Для сравнения, дотнет ничего не ищет, тупо исполняет:
Program:IsMax(int):bool:
       lea      eax, [rdi+01H]
       cmp      eax, edi
       setl     al
       movzx    rax, al
       ret

Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.


S>Если бы мог, то он бы просто заменил всю арифметику на сравнение с единственным (для каждого типа) верным решением.


Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.
Отрасль не поймёт. ))

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

Плюс, некоторые UB довольно-таки, забавны.
Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.
Например, в C#:
https://godbolt.org/z/rnaxjo4Kv
using System;

class SomeClass {
    public void Method() {
        Console.WriteLine("!!!");
    }
}

class Program
{
    static void Main()
    {
        SomeClass? sc = null;
        var s = Console.ReadLine();
        if(s.Length > 1)
          sc = new SomeClass();

        sc.Method();
    }
}

Вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный:
cmp      byte  ptr [rbx], bl

(если в rbx будет null, то сгенерируется прерывание AV)

В плюсах это UB, но, например, в MSVC прекрасно работает:
#include <iostream>

using namespace std;

struct SomeObj {
    void Method() {
        std::cout << "!!!" << std::endl;
    }
};

int main()
{
    int i;
    cin >> i;

    SomeObj * obj = nullptr;

    if(i > 0)
        obj = new SomeObj();

    obj->Method();

    return 0;
}

Скомпиллировано как написано, условно создаётся объект obj, вызов метода заинлайнился.

Clang безусловно создаёт ненужный объект, потом вызывает заинлайненное тело.
GCC безусловно вызывает заинлайненное тело, не выделяя память в куче по new SomeObj().

И кто тут прав?
C#, который делает лишнюю проверку в рантайм перед вызовом чуть ли не каждого метода, хотя зачастую нет обращения к this?
MSVC, который исполняет ненужный бранчинг?
Clang, который создаёт ненужный объект?
Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))
Re[33]: Carbon
Здравствуйте, Sinclair, Вы писали:

CC>>INC EAX выставит SF при знаковом переполнении, так что CMP там в общем то и не нужен.

S>Нужен. Если мы сделаем INC на -8 (0xFFFFFFF8), то получится 0xFFFFFFF9, и SF==1.
S>Прыжки вокруг флагов, инкрементов, и вычитаний нужны только оттого, что компилятор не может статически решить уравнение x+1<x.

Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?

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

В дотнете я как-то показывал трюк эмуляции числовых-константных параметров шаблонов:
https://godbolt.org/z/fKvE14G33
using System;
using System.Runtime.CompilerServices;

interface IYesNo { 
    bool Value { get; }
}

struct Yes : IYesNo {
    public bool Value => true;
}

struct No : IYesNo {
    public bool Value => false;
}

class SomeType<TConfig> where TConfig : struct, IYesNo
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Method() {
        if(default(TConfig).Value) {
            Console.WriteLine("Yes");
        }
        else {
            Console.WriteLine("No");
        }
    }
}

class Program
{
    static void Main()  { 
        SomeType<Yes>.Method();
        SomeType<No>.Method();
    }
}

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

Кстате, в 8-м дотнете в сравнении с 5-6-ми, где я в последний раз проверял этот трюк, наконец-то убрали ненужную двойную инициализацию временного значения default(TConfig), где сгенерённый код выглядел полнейшим нубством.

Вот для 8-го дотнета:
Program:Main() (FullOpts):
       push     rbp
       mov      rbp, rsp
       mov      rdi, 0x7F2C74BD06E8      ; 'Yes'
       call     [System.Console:WriteLine(System.String)]
       mov      rdi, 0x7F2C74BD0708      ; 'No'
       call     [System.Console:WriteLine(System.String)]
       nop      
       pop      rbp
       ret


Вот для 6-го:
Program:Main():
       push     rbp
       sub      rsp, 16
       lea      rbp, [rsp+10H]
       mov      byte  ptr [rbp-08H], 0
       lea      rdi, bword ptr [rbp-08H]
       mov      byte  ptr [rdi], 0
       mov      rdi, qword ptr [(reloc)]
       mov      rdi, gword ptr [rdi]
       call     [System.Console:WriteLine(System.String)]
       mov      byte  ptr [rbp-10H], 0
       lea      rdi, bword ptr [rbp-10H]
       mov      byte  ptr [rdi], 0
       mov      rdi, qword ptr [(reloc)]
       mov      rdi, gword ptr [rdi]
       call     [System.Console:WriteLine(System.String)]
       nop      
       add      rsp, 16
       pop      rbp
       ret


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


S>То есть для signed типов он решение находит (просто неверное с т.з. человеческой логики), а для unsigned — увы.


Для сравнения, дотнет ничего не ищет, тупо исполняет:
Program:IsMax(int):bool:
       lea      eax, [rdi+01H]
       cmp      eax, edi
       setl     al
       movzx    rax, al
       ret

Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.


S>Если бы мог, то он бы просто заменил всю арифметику на сравнение с единственным (для каждого типа) верным решением.


Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.
Отрасль не поймёт. ))

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

Плюс, некоторые UB довольно-таки, забавны.
Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.
Например, в C#:
https://godbolt.org/z/rnaxjo4Kv
using System;

class SomeClass {
    public void Method() {
        Console.WriteLine("!!!");
    }
}

class Program
{
    static void Main()
    {
        SomeClass? sc = null;
        var s = Console.ReadLine();
        if(s.Length > 1)
          sc = new SomeClass();

        sc.Method();
    }
}

Вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный:
cmp      byte  ptr [rbx], bl

(если в rbx будет null, то сгенерируется прерывание AV)
А sc!.Method() убирает лишь ворнинг.

В плюсах это UB, но, например, в MSVC прекрасно работает:
#include <iostream>

using namespace std;

struct SomeObj {
    void Method() {
        std::cout << "!!!" << std::endl;
    }
};

int main()
{
    int i;
    cin >> i;

    SomeObj * obj = nullptr;

    if(i > 0)
        obj = new SomeObj();

    obj->Method();

    return 0;
}

Скомпиллировано как написано, условно создаётся объект obj, вызов метода заинлайнился, обращения к this нет, хотя вызов метода через nullptr.

Clang безусловно создаёт ненужный объект, потом вызывает заинлайненное тело.
GCC безусловно вызывает заинлайненное тело, никогда не выделяя память в куче для ветки obj = new SomeObj().

И кто тут прав?

C#, который делает лишнюю проверку в рантайм перед вызовом практически каждого метода (nullable-аннотация не помогает), хотя зачастую нет обращения к this?
Или MSVC, который исполняет ненужный бранчинг?
Или Clang, который создаёт ненужный объект?
Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))