1. C++ / Говнокод #15928

    +17

    1. 1
    2. 2
    3. 3
    4. 4
    5. 5
    template<class Container>
    void COW_guard(Container& forUnCow){
      const Container c={};
      cc+=c;
    }

    Запостил: LispGovno, 06 Мая 2014

    Комментарии (56) RSS

    • Сделайте мне это развидеть. Это для Qt в нашем проекте.
      Ответить
      • А что такое cc и как эта функция юзается?
        Ответить
        • Походу вместо сс+=cc надо читать forUnCow+=c, а эта штука запилена чтобы сделать detach() для данных векторов, мапов и т.п...

          Но зачем? Там какой-то баг с implicit share нашелся?
          Ответить
          • ОМайФакинГод.
            #define forUnCow сс

            Но зачем? о_О
            А пофиг. Я не из-за этого постил.
            COW_guard используется чтобы форсировать копирование контейнера в Qt после копирования, тк пока не было изменений и копировать его никто не собирается.
            Используется например в тех случаях когда используется std::unique_lock перед тем как разблокировать мьютекс, защищающий контейнер, в который недавно "скопировали" через COW данные из другого контенера. Чтобы внезапно в другом потоке не начались какие-нибудь копирования.
            Ну или используется например чтобы отдавать STL-итератор на контейнер, не боясь что он внезапно станет не валидным, после того как внезапно начнутся COW копирования.
            Ответить
            • > Чтобы внезапно в другом потоке не начались какие-нибудь копирования.
              Это вопрос производительности? Имхо, там вполне логично сделано - кто портит implicit shared контейнер со счетчиком больше 1, тот и платит за копирование его данных.

              Ну и если implicit sharing реально мешает, и всегда хочется именно копирования, а не шаринга, то почему бы не юзать тупые и надежные stl контейнеры, чем городить эти костыли с рукопашным detach'ем?
              Ответить
            • > отдавать STL-итератор на контейнер, не боясь что он внезапно станет не валидным, после того как внезапно начнутся COW копирования
              Если это неконстантный итератор - то детач произойдет сразу же, и счетчик станет равен одному. Главное отличие от STL - ни в коем случае нельзя самому делать копии того инстанса контейнера, у которого ты взял неконстантный итератор, пока этот итератор живы и используется. Т.к. тогда COW может случиться в момент записи через итератор:
              QVector v;
              auto it = v.begin();
              
              // замутили копию при живом итераторе
              QVector v2 = v;
              
              *it = 5; // кровь-кишки-распидорасило
              Если же это константный итератор - там вообще те же гарантии, что и в STL.

              Если я в чем-то ошибаюсь - поправь меня.
              Ответить
              • > Т.к. тогда COW может случиться в момент записи через итератор
                Тьфу ты. Наоборот же. COW не случится при записи через итератор, и пятерка попадет в расшаренный между v и v2 блок данных, что совсем не комильфо.
                QVector<int> v;
                v.push_back(1);
                QVector<int>::iterator it = v.begin();
                QVector<int> v2 = v;
                *it = 5;
                qDebug() << v; // QVector(5)
                qDebug() << v2; // QVector(5) ^_^
                Но суть это не меняет :)
                Ответить
            • И насчет расшаривания КоКоКонтейнеров для нескольких потоков. Да, вполне возможна ситуация, когда 2 контейнера в двух потоках одновременно войдут в detach(). Но, емнип, самое страшное, что при этом случится - каждый скопирует блок данных себе, а оригинальный блок освободится. Т.е. одно лишнее копирование.

              Сейча попробую найти пруфы, или же разубедить себя в этом ;)
              Ответить
              • // QArrayData
                bool isShared() const Q_DECL_NOTHROW
                {   
                    int count = atomic.load();
                    return (count != 1) && (count != 0);
                }
                
                inline bool isDetached() const { return !d->ref.isShared(); }
                // где d - QTypedArrayData, которая унаследована от QArrayData
                
                template <typename T>
                void QVector<T>::detach()
                {
                    if (!isDetached()) {
                        if (d->alloc)
                            reallocData(d->size, int(d->alloc));
                        else
                            d = Data::unsharableEmpty();
                    }
                    Q_ASSERT(isDetached());
                }
                
                // И в конце reallocData(), когда уже все скопировали
                if (!d->ref.deref()) {
                    // освобождаем старый блок данных
                }
                Т.е. при одновременном детаче оба треда увидят в счетчике 2, каждый сделает себе по копии, затем один из них уменьшит счетчик до 1, а второй до нуля и ёбнет оригинальный блок данных.

                Так что все потокобезопасно, хоть и возможно лишнее копирование.
                Ответить
                • Я просто зашел на говнокод, чтобы поплюсовать Бормонда. Сейчас ещё почитаю.
                  Ответить
                • Звучишь ты здраво. Это всё работает, пока stl итераторы не юзаешь. И вот тут коровья защита необходима.
                  Ответить
                  • > коровья защита необходима
                    http://tinyurl.com/cow-armor
                    Ответить
                  • > пока stl итераторы не юзаешь
                    Смотря как юзать... stl итераторы, если честно, ничуть не безопаснее обычных указателей...

                    А коровья защита (с точки зрения корректности проги) нужна только в одном случае - в момент, когда ты хочешь сделать снепшот контейнера при живых итераторах.

                    P.S. Жалко, что они не замутили метод clone(). Он бы решил эту проблему.
                    Ответить
    • A cow guard, or cattle guard, is a gateless entry through a fence...
      благодаря тебе узнал о такой занятной штуке как cattle grid
      Ответить
      • >gateless entry through a fence...
        Так fences в с++11 появились.
        Ответить
        • так то ж для скота!
          /green
          Ответить
          • А я о чем! Чтоб cow guard меньше городить.
            Ответить
            • Лол. Фенсы не помогут как замена коровьей защиты. Компилятор ничего не знает о коровьей оптимизации в библиотеке
              Ответить
              • Ну да. cow guard на дороге не заменит fence вокруг коровника. И наборот.
                Ответить
                • Вот именно. Вот если бы в крестах нормально продумали взаимодействия между забором потока и кастомными оптимизациями на уровне библиотек, то и такой проблемы бы не было. Вместо коровьего стража можно было бы использовать забор потока.
                  Ответить
                  • >Вместо коровьего стража можно было бы использовать забор потока.
                    А вот вообще интересная оптимизация выпилить вообще COW, как делают lock elision (выпиливают лочки и заборы вообще, когда она ненужны). Ведь COW часто делает бессмысленное копирование.

                    То есть если компилер/среда видит что объект при COW-мутации не утекает и более не используется никем. Самая частая ситуация, ссылка на старый херится, и подхватывается новый.

                    Типичный пример применения на немутабельной строке:
                    for (int i=0;i<n;++i) s+="1";

                    Так вот в этом случае, когда escape analisys показывает что объект не форкается, а старая версия превращается в новую, никакого COW не происходит, а мутируется старый объект и отдается ссылка на него.

                    Пример: в метод заходит COW-список, и в цикле начинятеся.
                    Обычный расклад: делаем копию на каждом добавлении.
                    Предлагается: компилятор понимает что внутри метода COW не нужен и делает его 1 раз - при входе в цикл.

                    Я джва года жду такую оптимизацию.
                    Ответить
                    • > на немутабельной строке
                      > никакого COW не происходит,
                      И правильно делает. Какое нахер COW на иммутабельной строке? COW оно только для мутабельных объектов, иначе какой в нем смысл.
                      Ответить
                      • > Какое нахер COW на иммутабельной строке?
                        В этом его смысл. Эту строку мы не меняем, потому что на неё могут ссылаться другие. А создаём копию, однако видится как мутирование.

                        >COW оно только для мутабельных объектов
                        Нет. Поправьте если я ошибаюсь, но суть COW создавать еще одну немутабельную копию, при любом изменении. Никакой COW-объект не меняется (иммутабельный), меняется только ссылка.
                        Ответить
                        • Если счетчик ссылок больше 1 - блок данных иммутабельный.
                          Если счетчик равен 1 (т.е. эксклюзивное владение) - еще как мутабелен.

                          Собственно почему оно и называется Copy On Write - сначала копируем ссылку на блок данных, в надежде на то, что никто не будет в него писать, а если все же кто-то собрался туда писать - делаем эксклюзивную копию.
                          Ответить
                          • Да я уже понял. Умно в qt сделали. Очень.
                            Я б не сказал что это говно, только с итераторами факап.

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

                            COW является надмножеством немутабельных объектов. Мы можем не городить схему со счетчиком ссылок, а тупо копировать на каждом write - оправдывая название. Это тоже кагбе COW. Читайте COW, в моих постах как создание иммутабельной копии.
                            Ответить
                            • > COW является надмножеством немутабельных объектов
                              Множество мутабельных объектов тоже является надмножеством иммутабельных ;)

                              А implicitly shared объекты пытаются сделать вид, что они value type - т.е. мутабельные и передаваемые по значению. Если бы не та фигня с итератором - их бы можно было отличить от настоящей передачи по значению только по косвенным признакам (времени копирования и занимаемой памяти).
                              Ответить
                        • Пример:
                          QString s = "test"; // s ссылается на блок данных со словом "test"
                          QString copy = s; // copy ссылается на тот же самый блок данных
                          
                          s[0] = '!'; // в этот момент будет проверен счетчик ссылок
                          // и т.к. у нас в нем 2 вызовется detach() который
                          // замутит отдельный блок данных для строки s
                          // скопирует в него данные из оригинального блока
                          // и уменьшит счетчик ссылок на старом на 1
                          
                          s[1] = '5'; // счетчик ссылок равен 1, на copy уже похуй
                          copy[1] = '6'; // счетчик ссылок тоже 1, на s никак не влияет
                          Ответить
                          • Уже понял. Это типа "оптимистичная" мутабельность. Если никто не юзает - мутируем, если конкуренция - копирует.
                            Примерно такая по духу техника ебашить спин-лок допустим 3 раза, если фейл - значит конкуренция и хватать лочку.

                            При отсутствии конкуренции будет дешевый спин-лок. При наличии спин-локи не будут тупить, а треды заснут до освобождения лочки.
                            Ответить
                    • > Я джва года жду такую оптимизацию.
                      Ну в кутишных строках почти так. Настоящий detach() с копированием будет ровно 1 раз на первой итерации твоего цикла. Дальше в цикле будут только недорогие (по крайней мере для х86) atomic load'ы и периодическое растягивание буфера, когда результат перестанет в него влезать.

                      Вот если бы еще компилятор смог бы выпилить эти load'ы... Хотя, емнип, на х86 они стоят столько же, сколько и не атомарные, если их трогает только один тред (а внутри цикла их трогает как раз таки один тред). Или я туплю?
                      Ответить
                      • >Ну в кутишных строках почти так.
                        Перечитал весь тред, пошёл прочитал доку и теперь до меня дошло.
                        Счетчик, о котором идёт речь - это счётчик ссылок на шаред блок. То счётчик, какой счётчик. Вот только кодеру надо писать это руками.

                        Только геморрой я написании такого кода. А я предлагаю как-то переложить эту работу на компилятор - если он стопудово уверен что ссылка всегда одна (объект в скопе), то должен просто мутировать немутабельное и подменить ссылку.
                        Ответить
                        • > это счётчик ссылок на шаред блок
                          Так точно.

                          > Вот только кодеру надо писать это руками.
                          Ну все-таки не самому кодеру, а автору либ типа Qt/boost/stl и т.п..

                          > Только геморрой я написании такого кода.
                          В том же Qt есть набор классов чтобы было легко мутить свои implicit shared класы - QSharedData и QSharedDataPointer. Порождаем от QSharedData класс, в который складываем все поля. Мутим внешний класс, в котором будут все методы, и одно поле - QSharedDataPointer<OurData> data. Вот и весь геморрой. Главно не забывать вызывать data.detach() во всех мутаторах.

                          > то должен просто мутировать немутабельное и подменить ссылку.
                          А откуда ему знать как мутировать какую-нибудь сложную структуру данных, если программист об этом не заботился, и писал код только для иммутабельного случая (с почленным копированием и всем таким)? В простых случаях может быть и разберется...
                          Ответить
                          • >а автору либ типа Qt/boost/stl и т.п..
                            Ну правильно. Тарасу.

                            >есть набор классов чтобы было легко мутить свои implicit shared класы - QSharedData и QSharedDataPointer. Порождаем от QSharedData класс, в который складываем все поля. Мутим внешний класс, в котором будут все методы, и одно поле - QSharedDataPointer<OurData> data. Вот и весь геморрой.
                            Ня!
                            Ответить
                          • >А откуда ему знать как мутировать какую-нибудь сложную структуру данных
                            Тут джва основных шага:
                            а) clone()
                            б) mutate
                            Вот оно должно выполнить mutate на старой ссылке. Возможно это должно быть завернуто в некую абстракцию/соглашение - пиши так и оно оптимизнёт, к примеру копирующий конструктор или какой-то clone.

                            Компилеры они ж сука умные, вот видит в методе он копирующий конструктор, а следом небольшое изменение скопированного объекта.
                            И понимает (когда инлайнит этот метод в место вызова), что ссылка на локальный объект херится и приведет к вызову деструктора и free.
                            Он выпиливает копирующий конструктор, выпиливает деструктор старого, оставляет мутирование, но только подменяет ссылку чтоб мутировало старую структуру.
                            Или это бред?
                            Ответить
                            • Х.з. в крестах куча все же опасней чем в жабе, да и аллокацию/деаллокацию программист может заменить на что душе угодно. Поэтому компилер скорее всего никогда не рискнет такую оптимизацию делать.

                              А вот на стеке он вполне справляется с выпиливанием лишних конструкторов копий (опять же, если они без побочных эффектов и не дрочат кучу).
                              Ответить
                              • >А вот на стеке он вполне справляется с выпиливанием лишних конструкторов копий
                                Заинлайненый скоп это ж и есть куча, или не?
                                То есть крестооптимизатор дуплит, что в таком коде:
                                class COW {
                                    COW(const COW& copy);
                                    inline COW mutate(int i){
                                        COW copy=this;
                                         copy.counter=i;
                                         return copy;
                                    }
                                }
                                ...
                                for (it=0;it<n;++i) x=x.mutate(it);


                                ссылку на старый x выбрасываем и raii должно дернуть деструктор. при этом оно понимает что нам нужна копия только что выброшенного объекта, после копирующего конструктора в метода сразу идёт деструктор .

                                И в таком случае эти бесполезные действия сгорают - какой смысл уничтожать объект и перед этим делать его копию?
                                Ответить
                                • Ну это должен оптимизнуть, тут кучи нет.

                                  А вот если конструктор или деструктор внутри делают new или delete - очень маловероятно, что он их выкинет. Все-таки для крестокомпилятора new и delete это черный ящик, о котором он ничего не знает.
                                  Ответить
                                  • А если в конструкторе/десрукторе появится сайд-эффект, типа высрать чего-то в консоль?

                                    >> для крестокомпилятора new и delete это черный ящик, о котором он ничего не знает.
                                    Потому что всё зависит от того как реализованы malloc/free?
                                    Ответить
                                    • > Потому что всё зависит от того как реализованы malloc/free?

                                      нет, потому, что new/delete можно переопределять.
                                      Ответить
                                      • >new/delete можно переопределять.
                                        Так компилятор увидит что там напереопределял программист.

                                        Не пойму что мешает сишко(кресто)компилятору понять, что вот есть malloc, вот есть memcpy в то место где выделили память, и вот есть free старого.

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

                                        Потому можно не делать malloc+memcpy+free, а вернуть старый указатель.
                                        Ответить
                                        • > компилятор увидит
                                          Вообще не факт. Особенно если они где-нибудь в соседней дллке.

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

                                          Тут разве что помечать пары new/delete и malloc/free специальным атрибутом, как бы намекая компилятору, что он может творить с их вызовами непотребства...
                                          Ответить
                                          • >К тому же аллокаторы там очень сложные
                                            Так а это всё не сокрыто в malloce?

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

                                            Не могу представить себе ситуацию при нормально работающем алокаторе (без сайд-эффектов) где копирование буфера и немедленное удаление старого не эквивалентно реюзу оного.
                                            Разве что пользовательские хуки отвалятся.
                                            Ответить
                                            • > Так а это всё не сокрыто в malloce?
                                              Не факт. Там ведь хоть для каждого класса можно свои new/delete запилить.

                                              > Не могу представить себе ситуацию при нормально работающем алокаторе (без сайд-эффектов) где копирование буфера и немедленное удаление старого не эквивалентно реюзу оного.
                                              Да, тут я соглашусь. Но только если компилятор сумеет доказать, что аллокатор и деаллокатор работают над блоком одного размера.
                                              Ответить
                                              • >Там ведь хоть для каждого класса можно свои new/delete запилить.
                                                3ачастую это хождение по тонкому льду, да и часто ли нужно их переопредлеять находясь в здравом уме? Я к тому что пусть в кастомные new/delete такая оптимизация и не сможет, но это малая толика всего кода.

                                                >только если компилятор сумеет доказать, что аллокатор и деаллокатор работают над блоком одного размера.
                                                Само собой. Плюс должен копироваться такой же размер.
                                                Хотя при разных размерах умный компилер может догадаться превратить malloc+memcpy+free в realloc.

                                                PS Короче в хацкелл такое надо.
                                                Ответить
                                                • > Хотя при разных размерах умный компилер может догадаться превратить malloc+memcpy+free в realloc.
                                                  Не-не-не девид блейн... Не забывай, что в вектора и мапы не только POD'ы пихают, но и сложные классы, которые нельзя просто так взять и передвинуть на другое место.

                                                  Да и реаллок почти всегда будет делать malloc+memcpy+free. В нем ведь адрес не меняется только если после предыдущего alloc'а не выделили что-то еще...

                                                  > Короче в хацкелл такое надо.
                                                  Возможно. Там язык более строгий, и структуры логически иммутабельны. Там может и взлететь.
                                                  Ответить
                                                  • >Не-не-не девид блейн...
                                                    Лол. Ну да.
                                                    >Там язык более строгий
                                                    И функции маленькие и без сайд-эффектов, и компилер всё время что-то выводит и доказывает, и САМОЕ ГЛАВНОЕ - отсутствуют UB и new/ delete - не переопределяются.
                                                    Ответить
                                        • Ну и сейчас в крестах 11 есть move семантика - вместо копирования можно передать блок данных от одного объекта другому (при этом старый станет пустым):
                                          std::vector<std::vector<int>> v;
                                          for (size_t i=0; i<10; ++i) {
                                              std::vector<int> row;
                                              for (size_t j=0; j<10; ++j)
                                                  row.push_back(42);
                                              // вот тут за счет мы избегаем alloc-copy-free
                                              v.push_back(std::move(row));
                                          }
                                          Ответить
                                          • Прикольно. Только не совсем пойму как это можно заюзать для оптимизации немутабельных объектов.
                                            Ответить
                                            • > Только не совсем пойму как это можно заюзать для оптимизации немутабельных объектов.
                                              146%, что никак, ибо это для оптимизации объектов мутабельных ;)

                                              После строчки v.push_back(std::move(row)); row станет пустым. Какая уж тут иммутабельность...
                                              Ответить
                  • >Вместо коровьего стража можно было бы использовать забор потока.
                    tl;dr. Резюмируя пост выше - не ставить, а даже убирать fence, lock и guard, там где заведомо нету коров и прочего скота.
                    Ответить
                    • А жабий jit же вроде умеет убирать лишние локи (если их там получается 2 подряд, вложенные друг в друга синхронайзеды с одним и тем же объектом и т.п.)?
                      Ответить
                      • >если их там получается 2 подряд, вложенные друг в друга синхронайзеды с одним и тем же объектом
                        Слияние подряд идущих захватов локов одного монитора это другая фича lock coarsening.

                        >жабий jit же вроде умеет убирать лишние локи
                        Вроде умеет. Но эти фичи долгое время были эксперементальными, так что не факт.
                        В общем если объект не попадает на кучу (юзается в пределах метода), и мы синхронизируемся на нём, самый тупой случай: synchronized (new Object()), то теоретически должно выпилить - это lock elision.


                        Но я предлагаю применять похожую оптимизацию к немутабельным объектам, делая мутацию внутри, вместо копирования, в том случае если ссылка немедленно заменяется на новую копию.
                        Ответить
                      • Да и gcc/icc умеют что-то эдакое, а недавно и обсираемый многими хасвелл научился.
                        Так что надеюсь скоро и амд осилит.
                        Ответить
    • Нахуй ваш "C++" нужен?
      Ответить

    Добавить комментарий