Кресты / Говнокод #15928 Ссылка на оригинал

0

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

Запостил: LispGovno LispGovno, (Updated )

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

  • Сделайте мне это развидеть. Это для Qt в нашем проекте.
    Ответить
      • Походу вместо сс+=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 итераторы не юзаешь. И вот тут коровья защита необходима.
                Ответить
                • > пока stl итераторы не юзаешь
                  Смотря как юзать... stl итераторы, если честно, ничуть не безопаснее обычных указателей...

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

                  P.S. Жалко, что они не замутили метод clone(). Он бы решил эту проблему.
                  Ответить
  • A cow guard, or cattle guard, is a gateless entry through a fence...
    благодаря тебе узнал о такой занятной штуке как cattle grid
    Ответить
          • Лол. Фенсы не помогут как замена коровьей защиты. Компилятор ничего не знает о коровьей оптимизации в библиотеке
            Ответить
            • Ну да. 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++, guest?!

    А не использовать ли нам bbcode?


    8