Блог пользователя kb.

Автор kb., 13 лет назад, По-русски

Здравствуйте! Я думаю, что почти все программисты, пишущие на C++, регулярно используют и STL. Библиотека, безусловно, замечательная, но в ней сокрыто немало тонкостей, которые могут неожиданно вылезти на контесте.


Например, совсем недавно я узнал, что vector<bool> хранит элементы в виде байтов, в каждом из которых содержится по 8 bool'ов, из-за чего не очень быстро (очень не быстро?) работает. Или, например, что вектор в Visual Studio расширяется в 1.5 раза, а вектор в g++ - в 2 раза. Поправьте меня, если я не прав.

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

Для начала мне хотелось бы узнать две вещи:
1) За сколько работают методы begin, end, size у сложных структур типо set, map.
2) Как писать лучше и быстрее: set.find(elem) != set.end() или set.count(elem) == 0
3) Правда ли, что stack, queue и deque очень медленно работают?
  • Проголосовать: нравится
  • +14
  • Проголосовать: не нравится

»
13 лет назад, # |
Rev. 4   Проголосовать: нравится +9 Проголосовать: не нравится

Надо быть аккуратнее с count когда юзаешь multiset, точнее лучше вообще в таком случае не использовать count. Для меня было когда-то очень неожиданным, что count в multiset работает за log(n) + количество считаемых элементов. Можно тут лицезреть это: http://ideone.com/SPYAh 

По поводу векторов, которые раздуваются в 1.5, 2 раза можно делать reserve, если знаешь примерное количество элементов в вектор.
»
13 лет назад, # |
  Проголосовать: нравится +9 Проголосовать: не нравится
1)За константу. У меня с этим проблем не было.
2)Про count уже сказали - от multiset'а не использовать. Так что с multi- лучше find, и в остальных случаях, думаю, без разницы.
3)Да. Когда их константа - это еще более-менее, но помимо этого они жрут очень много памяти. Например, 105 деков интов в ML не влезут. Лучше использовать либо ручками, либо list
»
13 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

По поводу deque - правда. Есть очень сильное подозрение (поправьте, если не прав), что deque - это обычный вектор с задефайненой операцией #define push_front(val) insert(d.begin(), val). Подозрение появилось после того как сдавая задачу на 0-1 обход заметил что работают вектор и дек одинаково.

  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится +5 Проголосовать: не нравится
    Вроде зависит от реализации, но судя по вот этому, дек работает всегда за константу. Видимо, очень большую :)
    • »
      »
      »
      13 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится
      Это странно, потому что когда я отсылал теститься на сервер, вектор и дек показывали абсолютно одинаковые результаты с точностью до 23 мс.
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    нет, дек - это не обычный вектор. у вектора выделение памяти всегда последовательное, у дека - нет.
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится +1 Проголосовать: не нравится
    У deque не простая реализация работы с памятью, из-за этого он проигрывает по скорости выполнения операций вектору. В книги “STL для программистов C++” Л. Амерааль  стр. 92 описано  подробней.
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Насколько я знаю,  в gcc deque реализован как список чанков фиксированного размера и двусторонний вектор указателей на эти чанки. Поэтому доступ осуществляется за O(1), оверхед по сравнению с вектором почти не заметен, но появляется оверхед по памяти, порядка половины размера чанка, поэтому заводить вектор из deque не стоит.
»
13 лет назад, # |
  Проголосовать: нравится +8 Проголосовать: не нравится
По тонкостям есть замечательная книжка Scott Meyers "Effective STL". Правда, конкретно перечисленные вопросы там, вроде, не рассматриваются.
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Спасибо, посмотрю. Мне не только эти вопросы интересны, я их просто в пример привел. Единственное что, хотелось бы услышать истории именно спортивных кодеров, так как в книжке наверняка освещаются вопросы, связанные в основном с промышленным кодом.
»
13 лет назад, # |
Rev. 2   Проголосовать: нравится +3 Проголосовать: не нравится

Может это и не имеет отношения к STL, но все же спрошу:


   1) Есть ли разница: объявлять переменные счетчиков в начале кода или же их можно объявлять непосредственно в цикле? (мне по душе второй вариант)

   2) Правда, что такая "++i" операция работает быстрее чем "i++"?
  • »
    »
    13 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится +3 Проголосовать: не нравится

    1) Разницу не замечал, но точно утверждать что ее нет не могу.

    2) В принципе да, потому что i++ - это {t = i; i = i+1; return t}, а ++i это {i = i+1; return i}. И на копирование и выделение новой переменной тратится немного времени. Но если это отдельная операция, то я уверен, что оптимизатор справится с заменой на более быстрый вариант.
       
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится +13 Проголосовать: не нравится
    Насчет счетчиков: объявлять их непосредственно в цикле гарантированно не хуже. Почему - поясню на примере x86-архитектуры. Глобальные переменные размещаются в DATA-секцию сразу после запуска программы, а локальные выделяются в текущий фрейм стека при выполнении инструкции ADD ESP, {размер фрейма}. При этом с точки зрения времени нет разницы, к примеру, между выделением 20 и 24-байтных фреймов.

    Есть более важный момент. Если переменная объявлена в заголовке цикла, то велика вероятность, что компилятор поместит ее в регистр, а это на производительности сказывается положительно (если тело цикла маленькое, разумеется).
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится +10 Проголосовать: не нравится
    1) На данный момент никакой разницы вы не увидите. Хотя в древних компилятарах, типа VS6.0 была какая-то проблема, но я её уже не помню. :)

    2) Для целочисленного типа в простых выражениях ++i и i++ раскроется в одно и тоже. А вот для итераторов лучше не использовать постфиксный инкремент, хотя на простых примерах вы не заметите разницы.
    • »
      »
      »
      13 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится
      Компилятор VS6.0 располагает переменную, объявленную в заголовке цикла не в контекст цикла, а контекст, содержащий цикл. Таким образом, два последовательных цикла с одинаковой объявляемой переменной просто не компилировалось из-за повторного объявления переменной. Не знаю, есть ли такой нюанс в каких-либо других компиляторов, но я по этой причине не использую объявление переменной в заголовках циклов. Кроме того, ИМХО так удобней, так как можно проверить, на чем остановился итератор после выхода из цикла (т. е. проверить, было ли выпадение по break)
  • »
    »
    13 лет назад, # ^ |
      Проголосовать: нравится -15 Проголосовать: не нравится
    2) для "простых типов"(int, double, long, ...) - одинаково работает
»
13 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится
Поделюсь печальным опытом.

Писал я осенью в качестве тренировки последний питерский четвертьфинал. Там была задача, в которой требовалось найти количество инверсий в массиве. Фенвик, компаратор к сортировке, сабмит и что-то вроде WA 4. Тестирую - все идеально. Пишу стресс-тест при N = 15, все опять работает. После конца контеста (задачу я писал последней) я ради интереса попробовал запустить стресс-тест на бОльшем массиве, и он упал. Оказалось, я в компараторе неправильно рассмотрел случай равенства значений, и он работал, если сортировка стабильна. Почему же это не отловилось? Опытным путем я потом выяснил, что std :: sort в моем компиляторе стабильна при N <= 16.