Pull to refresh
13
0.2

Пользователь

Send message

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

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

И не понимаю почему or_insert триггерит пессимизацию производительности в дебаге учитывая, что там обычный матч по собственному состоянию.

Ну так-то в данном конкретном месте матч, да, но потом через десятые руки в hashbrown::hash_map вызывается вот такое:

impl<'a, T, A> VacantEntry<'a, T, A>
where
    A: Allocator,
{
    pub fn insert(self, value: T) -> OccupiedEntry<'a, T, A> {
        let bucket = unsafe {
            self.table
                .raw
                .insert_tagged_at_index(self.tag, self.index, value)
        };
        OccupiedEntry {
            bucket,
            table: self.table,
        }
    }
}

Т.е. VacantEntry запоминает внутри себя что-то типа индекса букета и вставляет там значение в этот букет. На беглый взгляд вообще выглядит не так плохо с точки зрения производительности, но с точки зрения переложения этого механизма на C++ выглядит не очень.

Можете попробовать сравнить cpp с rust и посмотреть будет ли там деградация на коллизиях.

В текущем C++ API деградации на коллизиях просто неоткуда взяться, так как после единственного лукапа при insert мы сразу получаем итератор, указывающий на конкретную прямо KV-пару, и все на этом, дальше работа идет просто с этой KV-парой напрямую. Можно вообще взять на нее ссылку сразу, никто не мешает. Там все ясно и понятно. У раста же многое зависит от качества реализации вот этой прослойки всей, нужно с бутылкой засесть и потеть, если хочешь разобраться, не факт что какие-нибудь умельцы чего-нибудь не пессимизируют потом... Ну и конечно тот факт что Entry пожирает всю мапу до конца своего лайфтайма, тоже мягко говоря, не очень удобно.

В Rust такая задача решается как-то так:

А вы уверены, что решается именно эта задача? Я вот сейчас не поленился, написал тест с вырожденной хеш-функцией, намеренно создающей коллизии. И что-то как только я комментирую часть .or_insert(i), так сразу время выполнения примерно вдвое падает. Ну прямо очень похоже на отдельный лукап на вставке. Да и с чисто технической стороны вопроса, из описания всех этих or_insert следует, что они выполняют полноценную операцию вставки, включая лукап. Иначе согласно этому API бы приходилось сначала всегда вставлять какой-то элемент в букет, если элемента с соответствующим ключом нет, а если or_insert не вызван, то приходилось бы его удалять при уничтожении экземпляра Entry, верно? А если вставляемый экземпляр невозможно создать со значениями "по умолчанию", как быть? Вопросы, вопросы...

P.S. Есть конечно вариант не вставлять "пустое" значение, а просто найти букет и держать его, но тогда получается, что ты не можешь найти Entry, потом что-нибудь сделать с map (скажем, вставить что-то или удалить), а потом сделать что-то с сохраненной Entry. В расте эта проблема "решается" тем что Entry "одалживает" map в свое полное распоряжение, и ты ничего не сможешь с этим map сделать, пока экземпляр Entry не уничтожится. Ну, тоже такое себе в приложении к C++.

Сейчас очень частая задача вида "если элемента с таким-то ключом еще нет в мапе, то вставить начальное значение, если он есть - то инкрементировать его на N" благодаря текущему API решается за один лукап. Продемонстрируйте, пожалуйста, как такая задача будет решаться за один лукап "по аналогии со ржавым" или "через какой-нибудь insert_or_update()", а то лично мне например непонятно, в чем состоит идея и как эта идея что-то реально улучшит, за исключением "перехода на опционалы ради перехода на опционалы".

Кажется адекватное применение для опционалов.

Каким образом? Там же итератор возвращается всегда, независимо от того, был уже этот элемент в контейнере или нет, в этом весь смысл такого API.

Я программирую на расте и благодаря этой "лжи" даже не беспокоюсь о сегфолтах.

Любая проблема в unsafe коде раста (а если даже вы лично его не пишете, то в используемых вами разнообразных библиотеках его точно полно, как и в FFI-обертках к внешним библиотекам на других языках) может протечь куда угодно, и вы так же будете "неделями сидеть и искать, где какой-нибудь байт указателя перезаписывается", как афтар каментов выше пишет про C. А если настанет такой момент, когда забивать шуруп молотком "безопасного" раста больше будет нельзя по объективным причинам и вы решите написать более эффективный код, чем тот, что позволяет вам написать тупорылый борроу чекер, то вам придется писать собственный unsafe код, и вот тут вы окажетесь на минном поле в квадрате, потому что, как выше правильно написал другой афтар каментов, никто на самом деле точно не знает, что является или не является UB в unsafe подмножестве раста (в отличие от C где это худо-бедно описано в стандарте - да, кое-где можно придраться к "не совсем очевидным формулировкам", но тем не менее).

В мире машинного обучения и дегенеративного контента

Поправил, не благодарите.

Ну я веду речь вообще-то про выбор между "написать самому или доверить LLM". LLM вряд ли принципиально лучше джуна или хитрожопого погромиста, а врать умеет даже, пожалуй, поубедительнее последнего. Впрочем, дело вкуса.

И вся прелесть LLM в том, что по идее она сама должна скомбинировать нечто подобное по вашему требованию (промпту).

Вот именно что "по идее". Сгенерированное все равно потом потребуется проверить как минимум на предмет полноты и адекватности, и результат проверки вас вполне может неприятно удивить.

Все на свете тестами не покроешь, а разработка правильных тестов - это тоже своего рода искусство. Как простейший пример для баша - LLM не добавила кавычки "куда надо" (или, наоборот, добавила там где не надо), и в результате на одних данных скрипт будет работать, а на других нет. Нужно иметь представление о потенциальных подводных камнях, и разрабатывать тесты с их учётом, а это означает, что опять же нужны знания в предметной области.

Как минимум лично у меня есть проблема: делать ревью чужого кода мне тяжелее, чем написать аналогичный свой, особенно если манера письма отличается от моей. В случае LLM дело усугубляется их склонностью к галлюцинациям и вранью в стиле "чёрное - это белое". И в любом случае, будь то написание кода или ревью, нужно сначала досконально разобраться в проблеме, а если уж разобрался, то написать код - это чуть ли не самое простое.

Ну скажем так я лично считаю, что говорить плохое - это несколько дешево :) Вот взять и сделать так, чтобы все сказали "вах" и никто не поливал дерьмом - это гораздо сложнее. Я бы даже сказал что это практически невозможно :)

Заодно теперь непонятно почему нужно писать методы классов по старинке:

Хотя бы даже потому, что "по старинке" выглядит банально короче, если нужно написать один метод, а не несколько "почти одинаковых", но с разными квалификаторами. Я даже думаю, что большинство методов и дальше "по старинке" будут писаться :)

Ну, насколько я понимаю, разница в именовании проистекает из принципа работы. template for - это именно шаблон, по которому компилятор перегенерирует "тело цикла" для каждого элемента из указанного списка. А if constexpr сам по себе никакой генерацией кода не занимается, компилятор просто выбирает один из вариантов в соответствии со значением constexpr условия. По-моему достаточно логично.

Да вроде нормально взаимодействует. Как и с return внутри себя.

и перенес её действие до цикла

Вообще-то нет.

это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно

У вас не делается ничего подобного. Вставьте отладочную печать скажем в лямбду и непосредственно перед строкой с for и убедитесь.

Шаблоны тут вовсе не при чем. В C и C++ широко практикуется отделение объявления от реализации, и is_copy_constructible далеко не всегда может проверить, что именно делает copy constructor. Вот например без всяких шаблонов:

$ cat module.h
struct Base
{
    Base() = default;
    Base(Base const &) = delete;
};

struct Derived: public Base
{
    Derived() = default;
    Derived(Derived const&);
};

$ cat module.cpp
#include "module.h"

Derived::Derived(Derived const&) : Base() {}

$ cat main.cpp
#include <type_traits>
#include "module.h"

static_assert(std::is_copy_constructible_v<Derived>);

int main() {}

Как прикажете is_copy_constructible в main.cpp проверять, что именно делает конструктор копии, реализация которого находится в module.cpp? Что, если module.cpp существует только в виде какого-нибудь module.so?

Это здесь не при чем.

Это здесь при всем. Все ваши претензии по сути к этому и сводятся - мол, "неудобно" брать код под GPL, включать его в свой закрытый продукт и не делиться с сообществом. This is by design.

Опенсорс давно перешел

Если бы не GPL, никакого "опенсорса" в текущем его виде вовсе не было бы. Был бы только закрытый софт, облепленный EULA, как говно - мухами. Вы либо плоховато помните обстоятельства, при которых вообще появились GNU и FSF, либо просто не застали их в силу возраста.

Теоретически можно, а практически наверняка заглядывали.

переписать все с gpl на mit?

Ну тут кстати есть интересный момент. Сильно сомневаюсь, что разработчики coreutils на расте ну прямо никогда-никогда не заглядывали в исходники соответствующих GNU Coreutils, хехе.

1
23 ...

Information

Rating
3,046-th
Registered
Activity