Optymalizacja kompilacji Emscripten psuje wykonanie programu

0

Modyfikuję sobie pewien program w WASM, który kompiluję za pomocą Emscripten. Kompilację wykonuję poleceniem:

rm compiled/*
emcc -std=c++20 jakisprogram.cpp -s BUILD_AS_WORKER=1 -o compiled/program.js -s ALLOW_MEMORY_GROWTH

Oczywiście, wymienionych jest więcej plików z kodem źródłowym. Program działał jak chciałem, nie miałem żadnego problemu. Ostatnio zastosowałem optymalizację O3, a więc kompilowałem takim poleceniem:

rm compiled/*
emcc -O3 -std=c++20 jakisprogram.cpp -s BUILD_AS_WORKER=1 -o compiled/program.js -s ALLOW_MEMORY_GROWTH

Program też działał, nawet szybciej, wydajniej, wydawało się, że jest po temacie. Jednak po dłuższym testowaniu stwierdziłem błąd w tworzeniu struktury, i to naprawdę dziwny błąd, który nie ma prawa się zdarzyć, jak się wprost analizuje kod źródłowy. Tak naprawdę, błąd jest już przy -O1, a także przy -O2.

W tym przypadku, błąd jest w tym konstruktorze, który tworzy obiekt, ale ten obiekt jest polem innej klasy.

TerminalKeyboard::TerminalKeyboard()
{
    std::vector<std::string> TerminalKeysCAS;
    TerminalKeysCAS.push_back("Up");
    TerminalKeysCAS.push_back("Down");
    TerminalKeysCAS.push_back("Right");
    TerminalKeysCAS.push_back("Left");
    TerminalKeysCAS.push_back("F1");
    TerminalKeysCAS.push_back("F2");
    TerminalKeysCAS.push_back("F3");
    TerminalKeysCAS.push_back("F4");
    TerminalKeysCAS.push_back("F5");
    TerminalKeysCAS.push_back("F6");
    TerminalKeysCAS.push_back("F7");
    TerminalKeysCAS.push_back("F8");
    TerminalKeysCAS.push_back("F9");
    TerminalKeysCAS.push_back("F10");
    TerminalKeysCAS.push_back("F11");
    TerminalKeysCAS.push_back("F12");
    TerminalKeysCAS.push_back("Home");
    TerminalKeysCAS.push_back("End");
    TerminalKeysCAS.push_back("Insert");
    TerminalKeysCAS.push_back("Delete");
    TerminalKeysCAS.push_back("PageUp");
    TerminalKeysCAS.push_back("PageDown");
    TerminalKeysCAS.push_back("F13");
    TerminalKeysCAS.push_back("F14");
    TerminalKeysCAS.push_back("F15");
    TerminalKeysCAS.push_back("F16");
    TerminalKeysCAS.push_back("F17");
    TerminalKeysCAS.push_back("F18");
    TerminalKeysCAS.push_back("F19");
    TerminalKeysCAS.push_back("F20");

    std::unordered_map<std::string, std::string> TerminalKeysTempl;

    // Keys depending on configuration 0
    TerminalKeysTempl["Up_CAS_0"] = "##_[_1_;_@_A";
    TerminalKeysTempl["Down_CAS_0"] = "##_[_1_;_@_B";
    TerminalKeysTempl["Right_CAS_0"] = "##_[_1_;_@_C";
    TerminalKeysTempl["Left_CAS_0"] = "##_[_1_;_@_D";
    TerminalKeysTempl["Up_CAS_1"] = "##_[_1_;_@_A";
    TerminalKeysTempl["Down_CAS_1"] = "##_[_1_;_@_B";
    TerminalKeysTempl["Right_CAS_1"] = "##_[_1_;_@_C";
    TerminalKeysTempl["Left_CAS_1"] = "##_[_1_;_@_D";

    // Keys depending on configuration 1 and 2
    TerminalKeysTempl["F1_CAS_0"] = "##_[_1_1_;_@_~";
    TerminalKeysTempl["F2_CAS_0"] = "##_[_1_2_;_@_~";
    TerminalKeysTempl["F3_CAS_0"] = "##_[_1_3_;_@_~";
    TerminalKeysTempl["F4_CAS_0"] = "##_[_1_4_;_@_~";
    TerminalKeysTempl["F5_CAS_0"] = "##_[_1_5_;_@_~";
    TerminalKeysTempl["F6_CAS_0"] = "##_[_1_7_;_@_~";
    TerminalKeysTempl["F7_CAS_0"] = "##_[_1_8_;_@_~";
    TerminalKeysTempl["F8_CAS_0"] = "##_[_1_9_;_@_~";
    TerminalKeysTempl["F9_CAS_0"] = "##_[_2_0_;_@_~";
    TerminalKeysTempl["F10_CAS_0"] = "##_[_2_1_;_@_~";
    TerminalKeysTempl["F11_CAS_0"] = "##_[_2_3_;_@_~";
    TerminalKeysTempl["F12_CAS_0"] = "##_[_2_4_;_@_~";

    TerminalKeysTempl["F13_CAS_0"] = "##_[_2_5_;_@_~";
    TerminalKeysTempl["F14_CAS_0"] = "##_[_2_6_;_@_~";
    TerminalKeysTempl["F15_CAS_0"] = "##_[_2_8_;_@_~";
    TerminalKeysTempl["F16_CAS_0"] = "##_[_2_9_;_@_~";
    TerminalKeysTempl["F17_CAS_0"] = "##_[_3_1_;_@_~";
    TerminalKeysTempl["F18_CAS_0"] = "##_[_3_2_;_@_~";
    TerminalKeysTempl["F19_CAS_0"] = "##_[_3_3_;_@_~";
    TerminalKeysTempl["F20_CAS_0"] = "##_[_3_4_;_@_~";

    TerminalKeysTempl["F1_CAS_1"] = "##_[_1_;_@_P";
    TerminalKeysTempl["F2_CAS_1"] = "##_[_1_;_@_Q";
    TerminalKeysTempl["F3_CAS_1"] = "##_[_1_;_@_R";
    TerminalKeysTempl["F4_CAS_1"] = "##_[_1_;_@_S";
    TerminalKeysTempl["F5_CAS_1"] = "##_[_1_;_@_T";
    TerminalKeysTempl["F6_CAS_1"] = "##_[_1_;_@_U";
    TerminalKeysTempl["F7_CAS_1"] = "##_[_1_;_@_V";
    TerminalKeysTempl["F8_CAS_1"] = "##_[_1_;_@_W";
    TerminalKeysTempl["F9_CAS_1"] = "##_[_1_;_@_X";
    TerminalKeysTempl["F10_CAS_1"] = "##_[_1_;_@_Y";
    TerminalKeysTempl["F11_CAS_1"] = "##_[_1_;_@_Z";
    TerminalKeysTempl["F12_CAS_1"] = "##_[_1_;_@_[";

    TerminalKeysTempl["F13_CAS_1"] = "##_[_1_;_@_\\";
    TerminalKeysTempl["F14_CAS_1"] = "##_[_1_;_@_]";
    TerminalKeysTempl["F15_CAS_1"] = "##_[_1_;_@_^";
    TerminalKeysTempl["F16_CAS_1"] = "##_[_1_;_@__";
    TerminalKeysTempl["F17_CAS_1"] = "";
    TerminalKeysTempl["F18_CAS_1"] = "";
    TerminalKeysTempl["F19_CAS_1"] = "";
    TerminalKeysTempl["F20_CAS_1"] = "";

    // Keys depending on configuration 3
    TerminalKeysTempl["Insert_CAS_0"] = "##_[_2_;_@_~";
    TerminalKeysTempl["Delete_CAS_0"] = "##_[_3_;_@_~";
    TerminalKeysTempl["Home_CAS_0"] = "##_[_1_;_@_~";
    TerminalKeysTempl["End_CAS_0"] = "##_[_4_;_@_~";
    TerminalKeysTempl["PageUp_CAS_0"] = "##_[_5_;_@_~";
    TerminalKeysTempl["PageDown_CAS_0"] = "##_[_6_;_@_~";
    TerminalKeysTempl["Insert_CAS_1"] = "##_[_2_;_@_~";
    TerminalKeysTempl["Delete_CAS_1"] = "##_[_3_;_@_~";
    TerminalKeysTempl["Home_CAS_1"] = "##_[_1_;_@_H";
    TerminalKeysTempl["End_CAS_1"] = "##_[_1_;_@_F";
    TerminalKeysTempl["PageUp_CAS_1"] = "##_[_5_;_@_~";
    TerminalKeysTempl["PageDown_CAS_1"] = "##_[_6_;_@_~";
    TerminalKeysTempl["Insert_CAS_2"] = "##_[_2_;_@_~";
    TerminalKeysTempl["Delete_CAS_2"] = "##_[_3_;_@_~";
    TerminalKeysTempl["Home_CAS_2"] = "##_[_1_;_@_H";
    TerminalKeysTempl["End_CAS_2"] = "##_[_1_;_@_F";
    TerminalKeysTempl["PageUp_CAS_2"] = "##_[_5_;_@_~";
    TerminalKeysTempl["PageDown_CAS_2"] = "##_[_6_;_@_~";

    // Keys for VT52 mode
    TerminalKeysTempl["Up_000_9"] = "##_A";
    TerminalKeysTempl["Down_000_9"] = "##_B";
    TerminalKeysTempl["Right_000_9"] = "##_C";
    TerminalKeysTempl["Left_000_9"] = "##_D";
    TerminalKeysTempl["F1_000_9"] = "##_P";
    TerminalKeysTempl["F2_000_9"] = "##_Q";
    TerminalKeysTempl["F3_000_9"] = "##_R";
    TerminalKeysTempl["F4_000_9"] = "##_S";
    TerminalKeysTempl["F5_000_9"] = "##_T";
    TerminalKeysTempl["F6_000_9"] = "##_U";
    TerminalKeysTempl["F7_000_9"] = "##_V";
    TerminalKeysTempl["F8_000_9"] = "##_W";
    TerminalKeysTempl["F9_000_9"] = "##_X";
    TerminalKeysTempl["F10_000_9"] = "##_Y";
    TerminalKeysTempl["F11_000_9"] = "##_Z";
    TerminalKeysTempl["F12_000_9"] = "##_[";

    TerminalKeysTempl["F13_000_9"] = "##_\\";
    TerminalKeysTempl["F14_000_9"] = "##_]";
    TerminalKeysTempl["F15_000_9"] = "##_^";
    TerminalKeysTempl["F16_000_9"] = "##__";
    TerminalKeysTempl["F17_000_9"] = "";
    TerminalKeysTempl["F18_000_9"] = "";
    TerminalKeysTempl["F19_000_9"] = "";
    TerminalKeysTempl["F20_000_9"] = "";

    TerminalKeysTempl["Insert_000_9"] = "##_L";
    TerminalKeysTempl["Delete_000_9"] = "##_M";
    TerminalKeysTempl["Home_000_9"] = "##_H";
    TerminalKeysTempl["End_000_9"] = "##_E";
    TerminalKeysTempl["PageUp_000_9"] = "##_I";
    TerminalKeysTempl["PageDown_000_9"] = "##_G";

    std::unordered_map<std::string, std::string> TerminalKeys_;

    // Generating codes for CAS modifiers
    for (int i = 0; i < TerminalKeysCAS.size(); i++)
    {
        std::string N = TerminalKeysCAS[i];
        for (int ii = 0; ii < 3; ii++)
        {
            std::string N_ = std::to_string(ii);
            if (TerminalKeysTempl.count(N + "_000_" + N_) > 0)
            {
                std::string NN = TerminalKeysTempl[N + "_CAS_" + N_];
                TerminalKeys_[N + "_001_" + N_] = TextWork::StringReplace(NN, "_@", "_2");
                TerminalKeys_[N + "_010_" + N_] = TextWork::StringReplace(NN, "_@", "_3");
                TerminalKeys_[N + "_011_" + N_] = TextWork::StringReplace(NN, "_@", "_4");
                TerminalKeys_[N + "_100_" + N_] = TextWork::StringReplace(NN, "_@", "_5");
                TerminalKeys_[N + "_101_" + N_] = TextWork::StringReplace(NN, "_@", "_6");
                TerminalKeys_[N + "_110_" + N_] = TextWork::StringReplace(NN, "_@", "_7");
                TerminalKeys_[N + "_111_" + N_] = TextWork::StringReplace(NN, "_@", "_8");
            }
        }
        if (TerminalKeysTempl.count(N + "_000_9") > 0)
        {
            TerminalKeys_[N + "_000_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_001_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_010_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_011_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_100_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_101_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_110_9"] = TerminalKeysTempl[N + "_000_9"];
            TerminalKeys_[N + "_111_9"] = TerminalKeysTempl[N + "_000_9"];
        }
    }

    // Common keys
    TerminalKeys_["Escape"] = "1B";
    TerminalKeys_["Tab"] = "09";
    TerminalKeys_["Enter_0"] = "0D";
    TerminalKeys_["Enter_1"] = "0D0A";
    TerminalKeys_["Enter_2"] = "0A";
    TerminalKeys_["Enter_3"] = "0D0A";
    TerminalKeys_["Backspace_0"] = "7F";
    TerminalKeys_["Backspace_1"] = "08";

    // Keys depending on configuration 0
    TerminalKeys_["Up_000_0"] = "##_[_A";
    TerminalKeys_["Down_000_0"] = "##_[_B";
    TerminalKeys_["Right_000_0"] = "##_[_C";
    TerminalKeys_["Left_000_0"] = "##_[_D";
    TerminalKeys_["Up_000_1"] = "##_O_A";
    TerminalKeys_["Down_000_1"] = "##_O_B";
    TerminalKeys_["Right_000_1"] = "##_O_C";
    TerminalKeys_["Left_000_1"] = "##_O_D";

    // Keys depending on configuration 1 and 2
    TerminalKeys_["F1_000_0"] = "##_[_1_1_~";
    TerminalKeys_["F2_000_0"] = "##_[_1_2_~";
    TerminalKeys_["F3_000_0"] = "##_[_1_3_~";
    TerminalKeys_["F4_000_0"] = "##_[_1_4_~";
    TerminalKeys_["F5_000_0"] = "##_[_1_5_~";
    TerminalKeys_["F6_000_0"] = "##_[_1_7_~";
    TerminalKeys_["F7_000_0"] = "##_[_1_8_~";
    TerminalKeys_["F8_000_0"] = "##_[_1_9_~";
    TerminalKeys_["F9_000_0"] = "##_[_2_0_~";
    TerminalKeys_["F10_000_0"] = "##_[_2_1_~";
    TerminalKeys_["F11_000_0"] = "##_[_2_3_~";
    TerminalKeys_["F12_000_0"] = "##_[_2_4_~";

    TerminalKeys_["F13_000_0"] = "##_[_2_5_~";
    TerminalKeys_["F14_000_0"] = "##_[_2_6_~";
    TerminalKeys_["F15_000_0"] = "##_[_2_8_~";
    TerminalKeys_["F16_000_0"] = "##_[_2_9_~";
    TerminalKeys_["F17_000_0"] = "##_[_3_1_~";
    TerminalKeys_["F18_000_0"] = "##_[_3_2_~";
    TerminalKeys_["F19_000_0"] = "##_[_3_3_~";
    TerminalKeys_["F20_000_0"] = "##_[_3_4_~";

    TerminalKeys_["F1_000_1"] = "##_O_P";
    TerminalKeys_["F2_000_1"] = "##_O_Q";
    TerminalKeys_["F3_000_1"] = "##_O_R";
    TerminalKeys_["F4_000_1"] = "##_O_S";
    TerminalKeys_["F5_000_1"] = "##_O_T";
    TerminalKeys_["F6_000_1"] = "##_O_U";
    TerminalKeys_["F7_000_1"] = "##_O_V";
    TerminalKeys_["F8_000_1"] = "##_O_W";
    TerminalKeys_["F9_000_1"] = "##_O_X";
    TerminalKeys_["F10_000_1"] = "##_O_Y";
    TerminalKeys_["F11_000_1"] = "##_O_Z";
    TerminalKeys_["F12_000_1"] = "##_O_[";

    TerminalKeys_["F13_000_1"] = "##_O_\\";
    TerminalKeys_["F14_000_1"] = "##_O_]";
    TerminalKeys_["F15_000_1"] = "##_O_^";
    TerminalKeys_["F16_000_1"] = "##_O__";
    TerminalKeys_["F17_000_1"] = "";
    TerminalKeys_["F18_000_1"] = "";
    TerminalKeys_["F19_000_1"] = "";
    TerminalKeys_["F20_000_1"] = "";

    // Keys depending on configuration 3
    TerminalKeys_["Insert_000_0"] = "##_[_2_~";
    TerminalKeys_["Delete_000_0"] = "##_[_3_~";
    TerminalKeys_["Home_000_0"] = "##_[_1_~";
    TerminalKeys_["End_000_0"] = "##_[_4_~";
    TerminalKeys_["PageUp_000_0"] = "##_[_5_~";
    TerminalKeys_["PageDown_000_0"] = "##_[_6_~";
    TerminalKeys_["Insert_000_1"] = "##_[_2_~";
    TerminalKeys_["Delete_000_1"] = "##_[_3_~";
    TerminalKeys_["Home_000_1"] = "##_[_H";
    TerminalKeys_["End_000_1"] = "##_[_F";
    TerminalKeys_["PageUp_000_1"] = "##_[_5_~";
    TerminalKeys_["PageDown_000_1"] = "##_[_6_~";
    TerminalKeys_["Insert_000_2"] = "##_[_2_~";
    TerminalKeys_["Delete_000_2"] = "##_[_3_~";
    TerminalKeys_["Home_000_2"] = "##_O_H";
    TerminalKeys_["End_000_2"] = "##_O_F";
    TerminalKeys_["PageUp_000_2"] = "##_[_5_~";
    TerminalKeys_["PageDown_000_2"] = "##_[_6_~";

    // Alternate numeric keys
    TerminalKeys_["NumPad_Numpad0_0"] = "##_O_p";
    TerminalKeys_["NumPad_Numpad1_0"] = "##_O_q";
    TerminalKeys_["NumPad_Numpad2_0"] = "##_O_r";
    TerminalKeys_["NumPad_Numpad3_0"] = "##_O_s";
    TerminalKeys_["NumPad_Numpad4_0"] = "##_O_t";
    TerminalKeys_["NumPad_Numpad5_0"] = "##_O_u";
    TerminalKeys_["NumPad_Numpad6_0"] = "##_O_v";
    TerminalKeys_["NumPad_Numpad7_0"] = "##_O_w";
    TerminalKeys_["NumPad_Numpad8_0"] = "##_O_x";
    TerminalKeys_["NumPad_Numpad9_0"] = "##_O_y";
    TerminalKeys_["NumPad_NumpadDecimal_0"] = "##_O_n";
    TerminalKeys_["NumPad_NumpadAdd_0"] = "##_O_l";
    TerminalKeys_["NumPad_NumpadSubtract_0"] = "##_O_m";
    TerminalKeys_["NumPad_NumpadEnter_0"] = "##_O_M";
    TerminalKeys_["NumPad_NumpadMultiply_0"] = "";
    TerminalKeys_["NumPad_NumpadDivide_0"] = "";

    TerminalKeys_["NumPad_Numpad0_1"] = "##_?_p";
    TerminalKeys_["NumPad_Numpad1_1"] = "##_?_q";
    TerminalKeys_["NumPad_Numpad2_1"] = "##_?_r";
    TerminalKeys_["NumPad_Numpad3_1"] = "##_?_s";
    TerminalKeys_["NumPad_Numpad4_1"] = "##_?_t";
    TerminalKeys_["NumPad_Numpad5_1"] = "##_?_u";
    TerminalKeys_["NumPad_Numpad6_1"] = "##_?_v";
    TerminalKeys_["NumPad_Numpad7_1"] = "##_?_w";
    TerminalKeys_["NumPad_Numpad8_1"] = "##_?_x";
    TerminalKeys_["NumPad_Numpad9_1"] = "##_?_y";
    TerminalKeys_["NumPad_NumpadDecimal_1"] = "##_?_n";
    TerminalKeys_["NumPad_NumpadAdd_1"] = "##_?_l";
    TerminalKeys_["NumPad_NumpadSubtract_1"] = "##_?_m";
    TerminalKeys_["NumPad_NumpadEnter_1"] = "##_?_M";
    TerminalKeys_["NumPad_NumpadMultiply_1"] = "";
    TerminalKeys_["NumPad_NumpadDivide_1"] = "";

    // Woraround to fix -O3 compilation bug
    //if (Screen::CurrentOpt < 0)
    //{
    //    //std::cout << "{" << TerminalKeys_["Right_000_1"] << "}" << std::endl;
    //}


    for (auto item : TerminalKeys_)
    {
        TerminalKeys[item.first] = item.second;

        bool IsError = false;
        std::string KeyStrTest = item.second;
        if ((KeyStrTest.length() % 2) != 0)
        {
            IsError = true;
        }
        for (int I = 0; I < KeyStrTest.length(); I++)
        {
            if ((KeyStrTest[I] < 32) || (KeyStrTest[I] > 126))
            {
                IsError = true;
            }
        }
        if (IsError)
        {
            std::cout << " [" << item.first << "]  [" << item.second << "]" << std::endl;
            //std::cout << "ERROR: [" << item.first << "]  [" << item.second << "]" << std::endl;
        }
    }
}

Tak naprawdę, jedynym celem konstruktora jest wypełnienie pola klasy std::unordered_map<std::string, std::string> TerminalKeys. Początkowo wypełniałem bezpośrednio globalne, teraz, jak widać, przekształciłem na wypełnianie lokalnej zmiennej, a na końcu przepisanie do globalnej. Użyta funkcja TextWork::StringReplace to jest "znajdź i zamień" w tekście, kod jakiś czas temu wziąłem z internetu:

std::string TextWork::StringReplace(std::string Str, std::string From, std::string To)
{
    int Pos = 0;
    while((Pos = Str.find(From, Pos)) != std::string::npos)
    {
       Str.replace(Pos, From.size(), To);
       Pos += To.length();
    }
    return Str;
}

Jednym z warunków koniecznych stwierdzenia poprawności wypełnienia TerminalKeys_ jest to, że każda wartość ma parzystą liczbę znaków i składa się z samych znaków ASCII. Stąd, na końcu konstruktora dopisałem sprawdzenie i wypisanie przypadków, które nie spełniają warunków. Co dziwne, wadliwe wypełnienie było na pozycji pod kluczem Right_000_0, więc teoretycznie pętla for (int i = 0; i < TerminalKeysCAS.size(); i++) nie ruszała, jednak usunięcie tej pętli sprawiało, że nie było błędu.

Teraz, przypadkiem stwierdziłem jeszcze dziwniejszą sprawę: Na końcu kodu testującego są dwie linie wypisujące, teraz jedną zakomentowałem, a drugą odkomentowałem uzyskując taki fragment:

        if (IsError)
        {
            //std::cout << " [" << item.first << "]  [" << item.second << "]" << std::endl;
            std::cout << "ERROR: [" << item.first << "]  [" << item.second << "]" << std::endl;
        }

Jak widać, te dwie linie tak naprawdę niczym się nie różnią, oprócz tego, ze jedna z nich wypisuje słówko ERROR. Jednakże, po tej zmianie i skompilowaniu z opcją -O3, nie ma żadnego błędu.

Odkomentowanie tego kawałka kodu również niweluje błąd. W praktyce warunek (Screen::CurrentOpt < 0) nigdy nie będzie spełniony, ale ważne jest to, że Screen::CurrentOpt jest ustawiany i zmieniany w trakcie pracy programu wśród kilku wartości dodatnich, żeby przypadkiem kompilator nie podstawił stałej w ramach optymalizacji.

    // Woraround to fix -O3 compilation bug
    if (Screen::CurrentOpt < 0)
    {
        std::cout << "{" << TerminalKeys_["Right_000_1"] << "}" << std::endl;
    }

Co bardzo ważne, jak nie ma optymalizacji, to nie ma żadnych problemów z programem, a ja jest już -O1, to już może zdarzyć się problem

Co może być przyczyną opisanego problemu? Czy to będzie wada w samym kompilatorze, czy gdzieś jakiś bardzo podstawowy błąd typu użycie niezainicjowanej zmiennej, zapisanie lub odczytanie wskaźnika NULL lub wskazującego przypadkowe miejsce (bardzo mało prawdopodobne, żebym gdzieś miał taki błąd, ale jakiś cień szansy istnieje), mimo, że w większości wykorzystuję shared_ptr, a zwykłe new/delete jest tylko w szczególnych, bardzo prostych przypadkach? Czy może jeszcze coś jeszcze innego?

A nawet, jeżeli faktycznie w projekcie mam jakieś bugi naruszające pamięć, to jak to jest możliwe, że w ramach jednej funkcji, dopiero co zapełniona struktura, przy wypisywaniu już jest zmieniona (i to jest błąd będący przedmiotem tego wątku)?

Jak przeniosłem ten kod do innej części programu, żeby łatwiej przetestować, to błędu nie było. W innej części programu też miałem jakiś drobny dziwny błąd, gdzie zaraz po utworzeniu listy napisów std::string, w jednym były błędne znaki i to też tylko przy -O1. Ale jak w całym programie zamieniłem wszystkie std::map na std::unordered_map, to błąd zniknął.

Czy optymalizacje O1, O2 i O3 są prowadzone na kodzie źródłowym (niekoniecznie na tekście kodu, ale na strukturze otrzymanej bezpośrednio po analizie składniowej, że można z niej odtworzyć kod źródłowy) przed kompilacją, czy optymalizacje są dokonywane na kodzie binarnym po kompilacji?

1

Doświadczenie mi podpowiada że
"cześciej mysli się człowiek niż kompilator"
"im dłużej szukam i odnajdę rozwiazanie problemu tym przyczyna błedu jest bardziej banalana"
Jezeli zamiana map -> unordered_map rozwiazała jakiś problem to by mi sie zapaliłą czerwona lampka że musze szukać dalej

Może najpierw dokłądnie sprawdz program w wersji C++ na zwykłym kompilatorze np.
https://github.com/google/sanitizers/wiki/AddressSanitizer ?

2

Opis objawów jest charakterystyczny, gdy gdzieś w kodzie jest Undefined Behavior (UB) (czy może stać się cokolwiek, działać crash dziwne zachowanie).
Bardzo często w trybie debug UB nie wychodzi, ale w trybie release już się manifestuje.
Mało tego czasami na Release też nie wychodzi, ale na konkretnym komputerze juz wychodzi.

Nigdy nie używałem Emscripten Compiler Frontend, ale to chyba jest jakaś podochocona Clang.
Moja rada, skompiluj program tak:

emcc -std=c++20 -Og -g jakisprogram.cpp -s BUILD_AS_WORKER=1 -o compiled/program.js -s ALLOW_MEMORY_GROWTH -fsanitize=address,undefined

A potem uruchom.
Kompilator powinien, dodać do kodu instrumentację, która będzie wykrywać większość błędów UB.
Program będzie 10 razy wolniejszy, ale jak wystąpi błąd, to dostaniesz ładny raport na czym polega problem.
Najlepiej wróć na forum z kopią tego raportu.

EDIT: widzę, że ten twój kompilator wspiera sanitizer-y:

Debugging with Sanitizers — Emscripten 3.1.66-git (dev) documentation

Debugging with Sanitizers

Undefined Behaviour Sanitizer

Clang’s undefined behavior sanitizer (UBSan) is available for use with Emscripten. This makes it much easier to catch bugs in your code.

To use UBSan, simply pass -fsanitize=undefined to emcc or em++. Note that you need to pass this at both the compile and link stages, as it affects both codegen and system libraries.
Debugging with Sanitizers — Emscripten 3.1.66-git (dev) documentation

......

Address Sanitizer

Clang’s address sanitizer (ASan) is also available for use with Emscripten. This makes it much easier to catch buffer overflows, memory leaks, and other related bugs in your code.

To use ASan, simply pass -fsanitize=address to emcc or em++. As with UBSan, you need to pass this at both the compile and link stages, as it affects both codegen and system libraries.

1

Przywróciłem wersję prezentowanego kodu, który powoduje błąd.

Do polecenia kompilacji dopisałem parametr -fsanitize=address,undefined , reszta pozostała bez zmian. W konsoli przeglądarki pokazała się informacja ze wskazaniem pliku i numeru linii, że na starcie programu, w zupełnie innym miejscu była próba uruchomienia metody na pustym obiekcie. Oprócz tej informacji była informacja o niepoprawnym dostępie do pamięci i rzucony wyjątek. Pomyślałem, że najpierw to poprawię, a potem będziemy dyskutować, jak problem nadal będzie występować.

Program faktycznie startował znacznie dłużej i działał ślamazarnie. Dokonałem niezbędnej poprawki, skompilowałem ponownie z tym parametrem i nie było błędu. Skompilowałem ponownie bez parametru, czyli tak, jak dotychczas, z optymalizacją O3 i nie ma błędu.

Problem był następujący


// Jest takie pole globalne, nazwijmy Core, klasa to CoreCommon.
std::unique_ptr<CoreCommon> Core;

// Tak tworzy się obiekt
Core = std::make_unique<CoreCommon>();

// Tak wywołuje się metodę na obiekcie
Core.get()->InitCommon();

Czy ja dobrze rozumiem, ze w C++, próba uruchomienia czegoś na sprytnym wskaźniku bez utworzenia obiektu jest UB i normalnie nie rzuca wyjątku?

Rozumiem, że testując program, najlepiej skompilować z -fsanitize=address,undefined i sprawdzić działanie wszystkich funkcji programu, bo takich niechcianych UB może być więcej i należy poprawić każdą zaraportowaną nieprawidłowość.

0
Marius.Maximus napisał(a):

Doświadczenie mi podpowiada że
"cześciej mysli się człowiek niż kompilator"
"im dłużej szukam i odnajdę rozwiazanie problemu tym przyczyna błedu jest bardziej banalana"
Jezeli zamiana map -> unordered_map rozwiazała jakiś problem to by mi sie zapaliłą czerwona lampka że musze szukać dalej

Może najpierw dokłądnie sprawdz program w wersji C++ na zwykłym kompilatorze np.
https://github.com/google/sanitizers/wiki/AddressSanitizer ?

Ja właśnie nie traktowałem zmiany typu struktury jako rozwiązanie problemu. Fakt, że ta zmiana ma wpływ na poprawność działania programu, sugeruje, że jest jakiś większy problem, no i był, już jedno UB stwierdziłem i poprawiłem.

1

Sanitizer to bardzo przydatna narzedzie
I lekcja na cale zycie zeby go uzywac ;)

1
andrzejlisek napisał(a):

Problem był następujący


// Jest takie pole globalne, nazwijmy Core, klasa to CoreCommon.
std::unique_ptr<CoreCommon> Core;

// Tak tworzy się obiekt
Core = std::make_unique<CoreCommon>();

// Tak wywołuje się metodę na obiekcie
Core.get()->InitCommon();

Czy ja dobrze rozumiem, ze w C++, próba uruchomienia czegoś na sprytnym wskaźniku bez utworzenia obiektu jest UB i normalnie nie rzuca wyjątku?

  • .get jest zbędny Core->InitCommon(); zadziała tak samo.
  • sprytny wskaźnik nie ma nic tu do rzeczy. Na surowym ustawionym an nullptr będzie tak samo. UB nastąpi tylko wtedy gdy nastąpi próba deferencji wskaźnika nullptr. Ergo można mięć member function, którą można wywołać na nullprt i nie mieć UB. Wystarczy, że funkcja ta: nie jet wirtualna i nie odnosi się do żadnych pól klasy i UB nie będzie.

Rozumiem, że testując program, najlepiej skompilować z -fsanitize=address,undefined i sprawdzić działanie wszystkich funkcji programu, bo takich niechcianych UB może być więcej i należy poprawić każdą zaraportowaną nieprawidłowość.

Najlepiej mieć napisane testy i uruchamiać je regularnie w rożnych konfiguracjach, w tym z włączonymi z sanitizer'ami.
Dobre praktyki testowania oprogramowania to dość szeroki temat: unit test, integration test, fuzz test, mutation test, performance test, test driven development.

0

Z powyższego wnioskuję, że zadziała się rzecz następująca:

  1. Kod zamieszczonym w temacie, w którym objawił się problem tak naprawdę ma dużo wartości stałych i kompilator przerobił go po swojemu, żeby efektem końcowym była mapa wypełniona określonymi danymi.
  2. Te wszystkie stałe zajmują stosunkowo dużo miejsca i kompilator wyznaczył jakiś tam zakres.
  3. Uruchomiła się funkcja na niezainicjowanym obiekcie będącym nullptr, która zapisuje kilka zmiennych (funkcja, którą sanitizer zasygnalizował zapisywała kilka zmiennych).
  4. Ponieważ obiekt nie był zainicjowany, zmienne miały zupełnie przypadkowe adresy.
  5. Jeden z tych adresów trafił się akurat w obszarze stałych wykorzystywanych w przedmiotowym kodzie.
  6. Później był uruchomiony ten kod, bez świadomości, że zawarte w nim stałe są już po cichu zmodyfikowane.
  7. Owa niechciana modyfikacja spowodowała, że niby poprawny kos wypełnił strukturę z błędami, bo posłużył się danymi żródłowymi z błędami.
0

Powinieneś przerwać analizę na punkcie 3 i naprawić kod w tym miejscu.
UB może prowadzić do takich absurdów, że mogą pojawić się "nosowe demony"

0
MarekR22 napisał(a):

Powinieneś przerwać analizę na punkcie 3 i naprawić kod w tym miejscu.
UB może prowadzić do takich absurdów, że mogą pojawić się "nosowe demony"

Tak właśnie zrobiłem, czyli zaraz po tym, jak sanitizer pokazał UB, to naprawiłem kod i przedmiotowy błąd przestał istnieć. Wcześniej po prostu nie wiedziałem, że w ten sposób można zbadać program, a błąd nie ujawniał się w momencie jego wystąpienia.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.