Почему 32769-я вставка не работает в std::unordered_set?

Я генерирую большое количество экземпляров класса и сохраняю их в файле std::unordered_set. Я определил хэш-функцию и отношение равенства, и пока все работает как надо — я вставляю 10000 экземпляров с unordered_set::insert, а могу их найти с unordered_set::find. Все объекты не повреждены, и нет никаких намеков на повреждение памяти или какие-либо другие проблемы.

Однако, когда я продолжаю вставлять, 32769-я вставка завершается ошибкой — она не генерирует, но возвращает пару, где итератор равен == nullptr (0x00000000). insert определяется как:

pair<iterator, bool> insert(const value_type& Val);

и обычно *iterator — это ключ, который я вставил, а логическое значение — true.
Если я (после ошибки) попытаюсь найти объект, он есть в наборе; если я попытаюсь вставить его снова, он скажет мне, что он уже там; так что вставка, кажется, работала нормально. Просто возвращаемое значение равно pair<nullptr,true> вместо pair<iterator,bool>.
Обратите внимание, что если я вручную заполню итератор и продолжу работу в отладчике, та же проблема повторится при первой вставке после 65536, а затем при 131072 и т. д. (то есть для 2 ^15+1, 2^16+1, 2^17+1, ...) - но не в 3 * 32768+1 и т. д.

Для меня это выглядит как переполнение short. Может у меня хэши совсем плохие и приводят к неравномерному заполнению бакетов, а на 32768 кончаются бакеты? Я не смог найти ничего более подробного о таком ограничении при поиске в Google, и я недостаточно знаю о сбалансированных деревьях или о том, что это внутри. он становится медленным и неэффективным, но он не должен отказывать.

Вопрос. Почему вставки 2^15+1, 2^16+1 и т. д. не работают и как этого избежать?

Это в Microsoft Visual Studio 2017 V15.7.1 (последняя версия от 15 мая 2018 г.). Компилятор настроен на использование правил C++ 2017, но я сомневаюсь, что это окажет какое-либо влияние.
Я не могу вставить полный код для минимально жизнеспособного решения, так как генерация объекта сложна для нескольких классов и методов и включает несколько сотен строк кода, сгенерированные хэши, очевидно, зависят от деталей объектов, и их нелегко воспроизвести в фиктивном коде.

### Обновление через один день ###: (я не могу указать это в ответе, потому что q был отложен) После обширной отладки стандартной библиотеки (включая много головоломок ), ответ @JamesPoag указывает на правильный ответ.
После вставки n я получаю:

  n     load_factor  max_load_factor  bucket_count  max_bucket_count
32766   0.999938965  1.00000000       32768         536870911 (=2^29-1)
32767   0.999969482  1.00000000       32768         536870911
32768   1.000000000  1.00000000       32768         536870911
32769   0.500000000  1.00000000       65536         536870911

неудивительно, что после 32768 вставок коэффициент загрузки достиг своего максимума. 32769-я вставка вызывает повторную хеширование в большую таблицу внутри внутреннего метода _Check_Size:

void _Check_size()
        {    // grow table as needed
        if (max_load_factor() < load_factor())

            {    // rehash to bigger table
            size_type _Newsize = bucket_count();

            if (_Newsize < 512)
                _Newsize *= 8;    // multiply by 8
            else if (_Newsize < _Vec.max_size() / 2)
                _Newsize *= 2;    // multiply safely by 2
            _Init(_Newsize);
            _Reinsert();
            }
        }

в конце вызывается _Reinsert() и заполняет все 32769 ключей в новые корзины и _устанавливает все указатели _next и _prev соответственно. Это работает нормально.
Однако код, вызывающий эти два, выглядит следующим образом (Plist – это название моего набора, этот код генерируется из шаблона):

_Insert_bucket(_Plist, _Where, _Bucket);

_TRY_BEGIN
_Check_size();
_CATCH_ALL
erase(_Make_iter(_Plist));
_RERAISE;
_CATCH_END

return (_Pairib(_Make_iter(_Plist), true));
}

Критическая точка находится в последней строке — для построения пары используется _Plist, но он содержит уже мертвый указатель на _next, потому что все адреса бакетов были перестроены в _Check_size() несколькими строками ранее. Я думаю, что это ошибка в std библиотеке — здесь ему нужно найти _Plist в новом наборе, где он выглядит так же, но имеет валидный указатель _next.

Простое «исправление» (проверено на работоспособность) заключается в расширении набора прямо перед критическим insert:
if (mySet.size() == mySet.bucket_count()) mySet.rehash(mySet.bucket_count() * 2);.

### Дальнейшее обновление: ### Я долго (более 16 часов) пытался создать минимальный код, воспроизводящий проблему, но пока не смог . Я попытаюсь зарегистрировать фактически рассчитанные хэши для существующего большого кода.
Я обнаружил, что одно хэш-значение одного из ключей изменилось (непреднамеренно) между вставкой и повторным хешированием. . Это может быть основной причиной; если я перенесу перефразирование за пределы вставки, проблема исчезнет.
Я не уверен, существует ли правило, согласно которому хэши должны быть постоянными, но, вероятно, это имеет смысл, как еще вы могли бы снова найти ключ.


person Aganju    schedule 18.05.2018    source источник
comment
Вы же не занимаетесь хакерством reinterpret_cast?   -  person user541686    schedule 18.05.2018
comment
Неа. Все чисто.   -  person Aganju    schedule 18.05.2018
comment
Хорошо. Оптимизированы ли ваши сборки вообще? (Даже встраивание?) Кроме того, пробовали ли вы пройти дизассемблирование, чтобы точно определить, какая инструкция изменяет значение, а затем вернуться, чтобы увидеть, где именно в исходном коде это находится? Если это не ваш собственный код, он, вероятно, либо оптимизирован, либо находится в коде, сгенерированном компилятором, поэтому вам необходимо выяснить, где это происходит.   -  person user541686    schedule 18.05.2018
comment
@Aganju: Я не могу вставить полный код для минимально жизнеспособного решения, поскольку [...] и его нелегко воспроизвести в фиктивном коде. Хотя я понимаю вашу дилемму, это почти невозможно для всех ( включая Microsoft), чтобы узнать, почему это происходит, не вдаваясь во все эти подробности. В конце концов, если бы вам пришлось заняться отладкой реализации, чтобы исправить ее, вам бы понадобился конкретный код, вызвавший проблему, верно?   -  person Nicol Bolas    schedule 18.05.2018
comment
Почему вы сравниваете итераторы с nullptr?   -  person user2357112 supports Monica    schedule 18.05.2018
comment
Работает на моей машине. Ошибка должна быть в коде, не предусмотренном.   -  person Eljay    schedule 18.05.2018
comment
@user2357112 user2357112 обычно нет, но потом происходит сбой, поэтому я проследил его до вызова библиотеки и убедился, что он возвращает nullptr в качестве итератора.   -  person Aganju    schedule 18.05.2018
comment
@NicolBolas я тебя слышу. Я надеялся, что кто-то, кто понимает, как работает std::unordered_set, или сталкивался с проблемой раньше, знает об этом. Если вы понимаете структуру, ограничение количества сегментов может иметь для вас значение, которого нет у меня.   -  person Aganju    schedule 18.05.2018
comment
@Mehrdad, это определенно происходит внутри вызова библиотеки std, и если вы когда-нибудь смотрели на этот код, его довольно трудно читать. Тем не менее, это будет мой следующий подход. Даже если я найду, это может мне не помочь - я не могу изменить стандартную библиотеку. Я лучше узнаю, что я могу сделать по-другому, чтобы избежать этого (сбалансировать ведра? Или что-то в этом роде, есть ли в этом смысл??)   -  person Aganju    schedule 18.05.2018
comment
Очень смешно, @Eljay. Я думал, что объяснил, почему я не могу предоставить полный код. Даже 32768 извлеченных и жестко закодированных хэшей — это довольно длинный код.   -  person Aganju    schedule 18.05.2018
comment
@Aganju: К сожалению, вам придется привыкнуть к стандартной библиотеке. Я делаю это на довольно регулярной основе сейчас. Раньше я даже не мог сказать, создано ли это человеком, но теперь я привык его читать. Если вы запутались в каком-то конкретном фрагменте их кода, просто ответьте мне, и я с радостью расшифрую его и объясню вам. Но, боюсь, я не вижу здесь другого решения.   -  person user541686    schedule 18.05.2018
comment
@Aganju: Скорее всего, это не проблема самой стандартной библиотеки, но пошаговое выполнение поможет вам выяснить, где (гипотетически) может происходить неявное приведение, которого вы не ожидаете, из-за того, как вы вызываете какую-то функцию . Это случилось со мной раньше. (Но, черт возьми, даже если это ошибка в стандартной библиотеке, это означает, что вы не хотите использовать ошибочную реализацию, поэтому вы хотели бы изменить стандартную библиотеку! Вам просто нужно будет сказать другим людям, чтобы они тоже пропатчили свою копию. Но в любом случае вам нужно пройти через это.)   -  person user541686    schedule 18.05.2018
comment
Хорошо, в ближайшие выходные... :-/ я сообщу здесь, когда найду. Если я найду это   -  person Aganju    schedule 18.05.2018
comment
Это 32-битная программа? Что такое sizeof(value_type)?   -  person Sid S    schedule 18.05.2018
comment
@Aganju: Конечно. Как только вы сообщите больше информации (чтобы у нас хотя бы была возможность увидеть, что происходит), дайте мне знать, если вам нужно повторное голосование; Я думаю, что смогу открыть его одним голосом.   -  person user541686    schedule 18.05.2018
comment
Вылетает ли ваше приложение (как и следовало ожидать), когда вы разыменовываете этот nullptr для доступа к узлу? Возможно, вы работаете на 64-битной системе и смотрите только на младшие 32 бита адреса?   -  person Eljay    schedule 18.05.2018
comment
Программа была запущена и протестирована на 32-битной системе. Я нашел проблему, см. редактирование выше.   -  person Aganju    schedule 19.05.2018
comment
Что означает итератор == nullptr? Вы не можете сравнивать итератор и nullptr_t.   -  person aschepler    schedule 19.05.2018
comment
это означает, что итератор возвращается из стандартной библиотеки со значением 0x00000000 (видно в отладчике). Это, конечно, ошибка, которая является основной проблемой этого вопроса. Я обнаружил ошибку в коде библиотеки std, которая приводит к этому недопустимому возвращаемому значению.   -  person Aganju    schedule 19.05.2018
comment
Теперь, когда вы знаете причину, вы сможете создать MCVE?   -  person M.M    schedule 19.05.2018


Ответы (2)


Я загрузил простой код на godbolt.org, чтобы увидеть результат, но у меня ничего не выскочило.

Я подозреваю, что значение вставлено и итератор создан, но вставка превышает max_load_factor и запускает перефразирование. В Rehash предыдущие итераторы становятся недействительными. Итератор возврата в этом случае может быть обнулен (или никогда не установлен) (опять же я не могу найти его в дизассемблере).

Проверьте load_value(), max_load_value() и Bucket_count() до и после ошибочной вставки.

person James Poag    schedule 18.05.2018

[это самостоятельный ответ]
Проблема не в стандартной библиотеке, как предполагалось, она в конце концов в моем коде (небольшой сюрприз). Вот что произошло:

Я вставляю сложные объекты в unordered_set, и хэш вычисляется из объекта. Допустим, у объекта 1 хеш H1, у объекта 2 хеш H2 и т. д.
Далее я временно модифицирую вставленный объект, клонирую его, вставляю клон в unordered_set и отмена модификации. Однако если insert запускает реорганизацию набора (что происходит при 2^15, 2^16 и т. д.), хэши всех существующих объектов пересчитываются. Поскольку объект 1 в настоящее время «временно изменен», его хэш возвращается не как H1, а другой. Это портит внутреннюю структуру набора и в конечном итоге возвращает недопустимый итератор. Псевдокод:

myMap.insert(Object1);  // hash H1 is internally calculated
Object1.DoChange();     // temporary modification
Object2 = Clone(Object1);
myMap.insert(Object2);  // <-- problem - rehashes internally and finds different hash H1 for Object1 !
Object1.UndoChange();   // too late, damage done

Проблема исчезает, если я перемещаю перефразирование за пределы insert или отменяю изменение объекта перед критической вставкой (поэтому хэш снова становится правильным).
Есть несколько других способов избежать этой проблемы (клонировать перед изменением, сохранить хеш-значение в объекте и не пересчитывать его и т. д.).

Основной урок: вычисление хэша должно быть стабильным. Вы не можете изменять объекты, находящиеся в наборе или сопоставлении, если это изменяет вычисленный хэш — набор или сопоставление могут вызвать повторное хеширование в неожиданные моменты времени.

person Aganju    schedule 23.05.2018
comment
Я уверен, что в ретроспективе это кажется очевидным, но спасибо за предупреждение и напоминание. Я столкнулся с чем-то подобным много лет назад, когда код модифицировал строку, делал с ней что-то, а затем модифицировал ее обратно. Проблема заключалась в том, что другой поток читал строку и видел изменения, которых не должно было быть. Не интересно отлаживать! - person Chris Uzdavinis; 23.05.2018