Programming
Rust
Swift
Comments 434
+1
Сам бинарник 62 Kb, но библиотеки runtime — 9 штук на сумму 54,6 Гб (я считал только те, без которых программа действительно не запускается)

Вы точно не ошиблись на 3 порядка? видимо, должно быть 54,6 Мб…
Просто рантайм на 54 гига даже в наше время избыточно жирных программ это что-то запредельно избыточное…
UFO landed and left these words here
+1
Не могли бы вы выложить файлы с данными для парсинга, чтобы попробовать запустить у себя?
+1
Если не лень качать. В принципе это просто размноженный фрагмент, приведенный в статье. В консоль должно вывести 2 записи.
0

Добавте ссылку в статью. или ссылку на код которым этот файл сгенерирован.

0
Rust действительно красавчик. Вы сравнивали потребление памяти? Возможно set в свифте дерево и его некорректно сравнивать с хешмапой, ведь оно требует куда большего кол-ва памяти, а свифт создан «для мобилок».
+1
Swift — VSZ: 205 Мб, RSS: 21 Мб
Rust — VSZ: 13 Мб, RSS: 1Мб
Все стабильно, не течет.
+6
«Некрасивость» кода на Rust в приведенном примере возникла исключительно из-за стиля написания. Например, обработку ошибки при открытии файла на Rust можно было написать в точности таким же `if let`, как на Swift. То же самое относится и к match в проверках на None. И еще много таких мест есть. Кажется, если поправить их все, код на Rust будет даже более красивым.
+3
Согласен, недавно только понял, что деструктуризация if let Ok(o) = val нагляднее, чем у других, так как сразу виден тип переменной val.
+6
Eще в таких же случаях методы типа .and_then() могут быть полезны. Или возврат ошибки из функции через оператор "?" с переносом обработки ошибок на уровень выше.

При чтении файла можно выкинуть всю возню с буферизацией и написать буквально так:
`for byte in BufReader(file).bytes()`.

Вместо `Option::None` можно писать просто `None`, так как это имя всегда импортировано. Аннотация типа в той же строке не нужна, Rust сам выведет. Вообще этот цикл лучше переписать как-то так:
`let dr = di.phones.iter().map(|p| res.index_by_phone.get(p)).any(Option::is_some);`

Тот же прием лучше использовать для итерирования по байтам: поскольку все непустые ветки завершаются одним и тем же `push()`, то имеет смысл профильтровать итератор, а потом вызвать `collect()`. Иногда такой подход повышает скорость, так как при заранее известном размере итератора вектор не будет переаллоцироваться.

Еще во многих местах лишний match на String, так как `val_to_str()` уже содержит точно то же самое. Вместо `to_string()` почти везде можно писать `into()`. В целом, кажется, я могу раза в два сократить написанное тут. И это даже если не вспоминать, что крейт `serde` содержит готовую работу с JSON, и вообще всю программу можно свести к одному вызову `from_str()`, если добавить `#[derive(Deserialize)]` к структурам. По скорости это будет очень быстро, Deserialize не делает лишних копирований и прочего.

Для переделки со строк на ссылки скорее всего достаточно просто сменить тип поля в структурах: сложная игра со временами жизни тут не требуется, достаточно просто формально это время объявить.
+2
Матч на стринг не лишний, если просто написать to_string(), то serde для строковых данных выведет еще дополнительные кавычки. За все остальное — большое спасибо!
+1

Кавычки внутри кавычек? Если так, то это где-то еще баг, и решать надо не таким способом. serde не дает дополнительных кавычек (точно знаю, у нас в продакшн он как раз на JSON). Если речь о скобках, не о кавычках, так там #[flatten] ставится. Чуть попозже внимательно проверю, откуда что идет, при беглом прочтении не вижу.

+2
Возможно это особенность реализации to_string() в serde_json::Value, похоже оно предназначено для презентационных целей, добавляет [], {}, "", надо просто другой метод исользовать.
+1
При чтении файла можно выкинуть всю возню с буферизацией и написать буквально так:
`for byte in BufReader(file).bytes()`.
=============
Неа, полученный байт придется анрапить, а это задержка существенная, проверял. Проще вообще читать сразу из File, своим собственным буфером, как товарищ ниже мой код переделал.
+3

Что бы чуть-чуть поменьше кода получилось в раст версии можно убрать impl Struct блоки и заменить из на:


#[derive(Default)]
struct Debtor { /*... */ }
// ...
Debtor::default();

Думаю, что было бы справедливо использовать тип process_file -> (isize, isize) или в swift поменять на Int32.


Ещё чисто из эстетических соображений убрать вложеность match std::fs::File::open(fname).


Возможно для swift версии будет иметь значение замена class на struct и соотвествующая замена let на var?

+1
Спасибо, не знал что дерайв дефолт и коллекции умеет инициализировать.
Структуры в Swift передаются по значению, копированием, поэтому только классы.
+3

Дерайвить дефолт можно для всех типов, которые имеют impl Default, а это почти все типы стандартной библиотеки. В том числе все коллекции, которые могут быть пустыми.

+1
Структуры в Swift передаются по значению, копированием, поэтому только классы.

Это не совсем так (точнее не всегда). Структуры в Swift используют семантику copy-on-write. То есть, в коде вида
let struct = Struct(var: 5)
var struct2 = struct
struct2.var = 10

структура скопируется не на 2 строке, а на 3. Также есть ключевое слово inout, которое используется для передачи внутрь метода структуры через аргумент и модификации её без копирования.

Собственно, для изменения вашего кода с классов на структуры, мне понадобилось добавить всего 1 строку помимо замены ключевого слова class на struct. По производительности мне это обошлось в 1 копирование в методе process_object (и то я не увидел никакой разницы).
-7
Вы сравнили ежа с ужом, как минимум языки под разные задачи, тогда уж сравнивайте C++ и java, результат будет похожий.
+3
Как-то писал похожий бенч Go vs Javascript (V8), там разница 2.5 раза всего, на Java давно не писал, не знаю.
+2
А если попробовать более по свифтовому?

import Foundation

struct DebtRec: Decodable {
    private enum Key: String, CodingKey { case company, debt, phones }
    private struct Company: Decodable { let name: String }
    
    enum Error: Swift.Error { case invalidCompany, invalidPhones, invalidPhoneItem, invalidDebt }
    
    let company: String, phones: [String], debt: Double
    
    private struct PhoneItem: Decodable {
        let value: String
        init(from decoder: Decoder) throws {
            let decoder = try decoder.singleValueContainer()
            guard let value = (try? decoder.decode(Int.self)).map({ "\($0)" }) ?? (try? decoder.decode(String.self)) else { throw Error.invalidPhoneItem }
            self.value = value
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        guard let company = (try? container.decode(String.self, forKey: .company)) ?? (try? container.decode(Company.self, forKey: .company).name) else { throw Error.invalidCompany }
        self.company = company
        guard let phones = (try? container.decode(Int.self, forKey: .phones)).map({ ["\($0)"] }) ?? (try? container.decode([PhoneItem].self, forKey: .phones).map({ $0.value })) else { throw Error.invalidPhones }
        self.phones = phones
        guard let debt = (try? container.decode(Double.self, forKey: .debt)) ?? (try? container.decode(String.self, forKey: .debt)).flatMap({ Double($0) }) else { throw Error.invalidDebt }
        self.debt = debt
    }
}

func main() throws {
    let data = try Data(contentsOf: URL(fileURLWithPath: "__path_to_json__"))
    let res = try JSONDecoder().decode([DebtRec].self, from: data)
}

try main()
+3
Не, ну так не честно.
VSZ: 1Гб
RSS: 920 Мб
Вы забрали весь файл в память, а в жизни приходится миллиарды записей обрабатывать.
+1
Это как бы решение из коробки, очевидно перед разработчиками swift не стояла задача супер оптимизации, в rust как известно наоборот.
+1
Ну, в общем, да. В браузере вообще невозможно файл потоково прочитать, и приходится ради такой мелочи ноду ставить :)
+2

В расте тоже есть решение из коробки let val = serde_json::from_str(file), только сравниваем мы вроде не стандартные функции работы с JSON, а более-менее эквивалентный код.

+2
Почему бы нам тогда не сравнить эквивалентный код на C++ и Java, и не порадоваться какой молодец C++?
-3
Там где java, с++ вообще не котируется и наоборот. По этому это Вы напишите, а мы почитаем.
0

А что мешает «по-честному» декодировать таким же способом, но построчно или чанками из буфера?

+5
C++ проиграет, если на Rust писать чисто. Потому что на ту игру со ссылками, которая нормальна в Rust с его borrow checker, ни один программист C++ в здравом уме не решится и будет копировать строки.

Сравнение Rust и Swift более чем корректно, потому что это очень похожие по синтаксису языки. Компилятор Swift мог бы быть таким же быстрым, как Rust, если бы его разработчики не халтурили.
+1
Насколько я понимаю, в свифте любые указатели это Arc<>, может поэтому и тормоза.
+1
Свифт ни разу не позиционируется как самый быстрый, и корректнее было бы его сравнивать с Kotlin Native, тот вроде тоже с ARC и под мобилки.
-16
C++ проиграет, если на Rust писать чисто.

Везде и всюду видно обратное и именно поэтому rust целиком и полностью С/С++-зависим.

ни один программист C++ в здравом уме не решится и будет копировать строки.

И что же мне помешает? Что мешает другим? Везде и всюду не копируются, а тут вдруг кто-то сообщают обратное.

К тому же, раст состоит целиком и полностью из копирования. Там копируется всё и единственное, что ему позволяет существовать — это llvm и его оптимизации, которые выпиливают все эти копирования.

Сравнение Rust и Swift более чем корректно, потому что это очень похожие по синтаксису языки.

Неверно. Раст целиком и полностью состоит из костылей из которых не состоит тот же свифт, та же жава, сишарп — тысячи их. Как минимум там нет исключений, а значит он никому ненужен.

Компилятор Swift мог бы быть таким же быстрым, как Rust, если бы его разработчики не халтурили.

Именно компилятор раста халтура, ведь раст существует только благодаря халяве, которую даёт llvm(т.е. С++). У раста вообще нет никакого компилятора — есть огрызок фронта, который так же завязан на llvm-рантайме.

Для понимания всей нелепости раста без халявы — достаточно взглянуть на код «без оптимизаций» — это то, что генерирует «компилятор» раста. Хотя об этом я уже говорил выше.
+7
Везде и всюду видно обратное и именно поэтому rust целиком и полностью С/С++-зависим.

Каким образом раст С-зависим? Ну не считая всяких врапперов, но это про любой язык сказать можно.


И что же мне помешает? Что мешает другим? Везде и всюду не копируются, а тут вдруг кто-то сообщают обратное.

Тут вкратце, почему не получится. Если в двух словах, то в плюсах это будет очень хрупко, и буст по перфомансу не окупит потенциальных проблем.


Неверно. Раст целиком и полностью состоит из костылей из которых не состоит тот же свифт, та же жава, сишарп — тысячи их. Как минимум там нет исключений, а значит он никому ненужен.

Ха-ха


Именно компилятор раста халтура, ведь раст существует только благодаря халяве, которую даёт llvm(т.е. С++). У раста вообще нет никакого компилятора — есть огрызок фронта, который так же завязан на llvm-рантайме.

Это плохо?

0
> Плохо это тем, что очередное пхп украв логику у llvm выдаёт достижения llvm за свои.
================
ТРЕБУЮ ОТЛИТЬ В ГРАНИТЕ!!! :))))))))))))))))))
0
Тут вкратце, почему не получится.

Эм… Не понял, в чем проблема. «If I wished to use a pointer I would need to be sure that the vector isn’t deallocated by the time I’m done with it» — для этого есть умные указатели. «And more importantly, to be sure that no other code pushes to the vector (when a vector overflows its capacity it will be reallocated, invalidating any other pointers to its contents)» — так не надо пихать в вектор сами строки, пихай туда указатели на строки в куче, resize вектора никак не повлияет на указатели на сами объекты. Более того, std мир не ограничивается, есть другие реализации, например, Qt с его implicit sharing, там копирование объектов из-за этого дешевое потому, что самого копирования там как такового нет (пока ты не захочешь изменить одну из копий), причем все это реализовано абсолютно прозрачно. В общем, странно мне это читать, это из разряда страшилок скорее.
+3

Умные указатели требуют затрат в рантайме. Поэтому на плюсах приходится выбирать между быстрыми сырыми указателями и медленными умными.

-1
Зато они работают везде, а не только в пределах вашего собственного кода.
0

Возвращаю вам ваш аргумент: и как же они будут работать, если владелец объекта — внешняя dll? :-)

0

Потому что внешняя dll в ситуации "codebase, так сказать, неполная" может удалить объект наплевав на все ваши умные указатели.

-7
А еще она может сделать rm -rf /. Ладно, в общем, вы скатываетесь в ту же демагогию, что и автор оригинальной статьи по ссылке, мне это не интересно. Просто имейте в виду для себя — когда вы пропагандируете rust, не делайте это так топорно. Для начинающих ваши аргументы могут показаться привлекательными, но для людей с опытом они выглядят… топорно и пропагандистски в плохом смысле. Я где-то понимаю, почему veslemay так хейтит пропагандистов rust'а :)
+1
То ли дело плюсы, там даже в пределах вашего собственного кода собранного разными версиями компилятора нихрена не гарантируется.
+1
для этого есть умные указатели

А вы их используете для каждой переменной? Я видел только использования, когда программист уверен, что тут они нужны.


так не надо пихать в вектор сами строки, пихай туда указатели на строки в куче

Что значит "не пихайте сами строки"? Строки это и так указатели на кучу где лежат данные, в лучшем случае они еще и длину хранят.


если есть такая опасность, то никто не мешает хранить в векторе такие же shared_ptr на атрибуты

Даже если использовать shared_ptr через shared_ptr отстрелить ногу вполне себе легко можно:


#include <memory>
#include <iostream>
#include <functional>

std::function<int(void)> f(std::shared_ptr<int> x) {
    return [&]() { return *x; };
}

int main() {
    std::function<int(void)> y(nullptr);
    {
        std::shared_ptr<int> x(std::make_shared<int>(4));
        y = f(x);
    }
    std::cout << y() << std::endl;
}
-1
А вы их используете для каждой переменной?

Нет, конечно. Но автор же беспокоится, что в large codebase некий код где-то там far far away, который, возможно, и не он сам писал, может как-то что-то прощелкать в плане времени жизни. Для таких случаев использование умных указателей безусловно оправданно.

Что значит «не пихайте сами строки»? Строки это и так указатели на кучу где лежат данные, в лучшем случае они еще и длину хранят.

Ну я не знаю, что автор тут имел в виду под «when a vector overflows its capacity it will be reallocated, invalidating any other pointers to its contents», видимо, он пытается сделать что-то вроде std::vector<std::string> (а потом брать ссылки на элементы вектора) вместо std::vector<std::string*>, в общем, выглядит это либо как бред начинающего, либо как передергивание.
+1
std::vector<std::string*> это потенциально тормоза из-за нарушения локальности данных, а это кэш промахи и вот это вот все. Строчки же еще имеют оптимизацию, когда для коротких строк символы будут лежать внутри самого объекта, а не где-то в куче. Да и вообще, не принято в плюсах хранить указатели в контейнерах, если только это реально не нужно прямо кровь из носа. Причин для этого масса, помимо локальности данных.

Автор ведет речь о слайсах, когда мы создаем указатель с длиной на внутреннее хранилище контейнера. Прямо как string_view, который все так долго ждали в плюсах. Только вот проблема, даже с учетом иммутабельности std::string мы с этими string_view запросто можем получить все вкусности плюсов — dangling pointer и use after free. А с std::vector дело еще хуже, ибо контейнер изменяемый и внутренний буфер могут внезапно уничтожить и реаллицировать в другом месте.

Поэтому и хвалит он раст, у которого компилятор не позволит такой баг допустить. В других языках такие вещи решаются всякими сборщиками мусора. В плюсах это решается внимательными программистами и фиксами CVE, когда неизбежно что-то внимательный программист пропустит.
0
Строчки же еще имеют оптимизацию, когда для коротких строк символы будут лежать внутри самого объекта, а не где-то в куче.

Не понял. Контейнер по условию задачи находится в куче, строки так или иначе тоже в куче — либо в составе контейнера, либо отдельно. То, что они не находятся в памяти друг за другом — так у автора есть же требование, что при расширении контейнера и реаллокациях ссылки на строки из него не должны инвалидироваться. Тут ничего не попишешь. Не было бы этого требования — можно было бы обеспечить для них локальность без проблем.
0
Не понял

Почитайте про short string optimization.

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

У него не требование, а факт того, что есть такая угроза и плюсы с ней никак не помогают. Раст помогает и ошибки допустить не позволит.
-1
Почитайте про short string optimization.

Вы… это самое… сами почитайте, о чем я пишу вообще.

Раст помогает и ошибки допустить не позволит.

Вы сильно преувеличиваете возможности компилятора раста что-то там «не позволить».
0
Вы… это самое… сами почитайте, о чем я пишу вообще.

Все недавние комментарии о том, что вы статью не поняли. Я это вижу, поэтому решил помочь, но чего-то не помогло все равно. По идее, на одном слове «слайсы» уже должно быть все понятно.

Вы сильно преувеличиваете возможности компилятора раста что-то там «не позволить».

Пустые слова. Раст не позволит, плюсы позволят. Так эти языки работают.
0
Я это вижу

Не похоже.

Пустые слова. Раст не позволит, плюсы позволят. Так эти языки работают.

Пустые слова — это считать компилятор раста серебряной пулей, способной заглядывать внутрь сисколлов или сторонних библиотек без прилагаемых исходников.
0
Поэтому у растаманов и принято линковать все статически с lto, а сошки это моветон.
-1
Ессессно. Им пока везет, что они тихо-мирно варятся там в своем растамирке, где нет необходимости решать реальные задачи с помощью сторонних closed source библиотек, а также делать свои такие же closed source библиотеки на продажу.
0
Прямо как string_view, который все так долго ждали в плюсах.

Все его использовали ещё со времён. Где именно его ждали? В стандарте? Ну слайсы для раста уже 10лет ждут в стандарте, как всё остальное.

Поэтому и хвалит он раст, у которого компилятор не позволит такой баг допустить.

Как? Не даст взять ссылку на элемент и придётся(как автор делал) хранить индексы? Это неудобно, но ладно.

Проблема заключается в том, что я без проблем могу написать vector, который ссылки не теряет. Причём мне ненужно даже писать вектор, можно просто поменять ему аллокатор.

Это одна из фундаментальные проблем «безопасности» подобного рода. Эта безопасность защищает даже тогда, когда никакой защиты ненужно. Но продолжает вносить оверхед(как и по производительности, так и по удобству программиста).
0
P.S. Прочитал там сносочку, кто-то рассказал гражданину про shared_ptr, но он начал придумывать новые страшилки — про «iterator invalidation» (пока ты держишь подконтрольную тебе копию shared_ptr, указывающую на вектор, вектор никуда не денется, и, соответственно, никакого iterator invalidation не будет), «someone consuming the API might take a reference of an attribute and hold on to it long enough for it to become invalidated» — если есть такая опасность, то никто не мешает хранить в векторе такие же shared_ptr на атрибуты: пока ты им владеешь, атрибут никуда не денется, даже если вектор будет полностью уничтожен. Например, такой код:

auto func()
{
    std::vector<std::shared_ptr<std::string>> vctor;

    vctor.push_back(std::make_shared<std::string>("FYVA"));

    return vctor.back();
}

int main()
{
    auto res = func();

    std::cout << *res << std::endl;

    return 0;
}

вполне себе успешно выведет «FYVA», хотя контейнер, содержавший строку, уже был уничтожен на момент вывода.

Более того, это будет совершенно безопасно работать даже в чужом коде, который доступен тебе исключительно в виде .lib/.so/.dll (насколько я понимаю, расту для его compile-time borrow checking нужен доступ к исходникам, ничего прочекать в чужом машинном коде он не в состоянии).

Это не говоря уж о том, что в том же Qt этот атрибут (если это какой-то стандартный QString или контейнер (QList/QMap/...) или даже какой-то кастомный наследник от QObject с реализованной implicit sharing функциональностью, это несложно) можно будет просто скопировать локально через обычный конструктор копии или operator=(), и не париться — будет shallow copy.

В общем, если по чесноку, аргументы автора выглядят, мягко говоря, надуманными и действительно похожими на пропаганду в плохом смысле этого слова.
+2
Iterator invalidation это про другое.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> a;
    a.push_back(1);
    a.push_back(2);
    for (int &b: a) {
        a.push_back(b);
    }
    std::cout << "Hello, UB!\n";
    for (int &b: a) {
        std::cout << b << "\n";
    }
}


Этот код компилируется без ошибок и предупреждений в GCC 4.9 c -Wall -Wextra -Wpedantic и выводит:

Hello, UB!
1
2
1
0


Некоторые операции над структурами инвалидируют итераторы, указывающие на эту структуру. Если после этого всё-таки использовать эти итераторы, то получаем undefined behavior.

Очень рекомендую проверить свой код на такие случаи.
0

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

0

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

-2
Ну вот не надо про это, что «компилятор следит». Что ваш компилятор сделает, если владелец контейнера — чужая .dll, к коду которой у вас нет доступа (а это совершенно рядовая ситуация, с которой сталкивается любой разработчик сплошь и рядом)? Ничего он не сделает и не проверит.
0

Как-то быстро вы перешли от двух локальных переменных к внешней dll...

0
Речь не идет о «двух локальных переменных». Автор сам пишет, что, дескать, «for a smaller codebase this might be possible» просто добавить в структуру указатель на вектор, но вот если codebase большая, дескать, то будут проблемы. А если codebase не просто большая, а еще и, так сказать, неполная?
+1

Если в документации на внешнюю dll не указано никакой информации о времени жизни своих объектов — такой dll, по-хорошему, вообще нельзя пользоваться.

0

Чужая dll это ансейф, и тут все то же самое, что в плюсах.


А где не ансейф, там гарантии работают.


Аргумент из разряда "а что если в компьютер с вашей программой прилетит метеорит".

0
Я к тому, что если вы пишете, прости господи, библиотеку, предоставляющую некий API, которую будут использовать как dll и, скорее всего, никто из использующих ее не будет ее каждый раз пересобирать, то вот эти вот все аргументы «а вдруг someone consuming the API might take a reference of an attribute and hold on to it long enough for it to become invalidated» — это как бы так себе аргумент, мягко говоря. Тут либо давать доступ к атрибутам через что-то вроде умных указателей, либо просто надеяться на то, что пользователи библиотеки напишут свой код аккуратно. Я думаю, в глубине души вы это и сами понимаете.
+1

Если я буду вызывать чужую dll, то я как и в плюсах лучше сделаю копию, на всякий случай.


Только это пессимистичный сценарий.


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

0
Если библиотека написана так, что ее функции принимают и возвращают умные указатели, то в «копии на всякий случай» смысла нет. Это лишний расход ресурсов.

Я помню функцию forEach, в которой в реализации копировался итерируемая коллекция, чтобы работало в случае инвалидации

Если эта forEach рассчитана на вызов чего-то внешнего, что ты в общем случае контролировать не можешь, то копирование — да, разумная предосторожность, что на C++, что на расте. foreach из Qt (который Q_FOREACH), например, по-любому делает shallow copy итерируемого контейнера, но там, как я уже писал, все продумано, и это дешевая операция. Если автор кода, который вызывается из foreach, молодец — то практически никаких затрат не будет, если не молодец — ну, будет дополнительное копирование.
0

Библиотека обычно написана в стиле ANSI C, потому что это единственный популярный язык со стабильным ABI. Откуда там возьмутся умные указатели — загадка.




А вообще, как я уже сказал, это все вилами по воде писано. Обычное приложение не линкуется к непонятным либам по звездам. Есть обычный пакетный менеджер, в нем есть нормальные зависимости, и вот это все. В таком сценарии компилятор все нормально проверит. И это дефолт.

0
Библиотеки разные бывают. Использовать ANSI C там никто не заставляет. Там может потребоваться собирать определенным компилятором, это да. А «обычное приложение» в моей практике сплошь и рядом линкуется к «непонятным либам», например, к Google AdMob, Firebase, Facebook SDK, и так далее.
0
Библиотеки разные бывают. Использовать ANSI C там никто не заставляет. Там может потребоваться собирать определенным компилятором, это да.

За пределами плюсов им собственно мало кто пользуется. Если я хочу из C# вызвать библиотечные функци апи с умными указателями мне никак не поможет.


А «обычное приложение» в моей практике сплошь и рядом линкуется к «непонятным либам», например, к Google AdMob, Firebase, Facebook SDK, и так далее.

Да пожалуйста


https://www.nuget.org/packages/Google.Apis/


https://www.nuget.org/packages/Facebook
...

0
Я к тому, что если у компилятора раста нет доступа к коду этих «непонятных либ» (хотя бы в виде IL), он вряд ли сможет что-то там прочекать с временем жизни объектов, передаваемых в них или возвращаемых из них. Даже если сами эти библиотеки будут написаны на Rust, а потом собраны в .dll, это никак им не поможет. Привет, unsafe.
+1

Эмм, если мы на плюсах подключаем С++ либы, то на расте мы подключаем раст либы. Если либа написана на расте, то компилятор её уже провалидировал.


Я не понимаю поинта.

0
Внутри она, возможно, и провалидирована, но вот в этой библиотеке, скажем, есть экспортированная функция, которая возвращает указатель на что-то. Компилятор в момент сборки этой библиотеки не может знать, как дальше этот указатель будет использован, и не может проверить, какое у него будет время жизни.
0

Валидировать время жизни указателя будет, очевидно, компилятор при сборке приложения, которая эту библиотеку использует. Библиотеке само собой об этом знать не нужно. Инкапсуляция, все дела.

0
Очевидно, он не сможет этого сделать без исходного кода библиотеки в том или ином виде. Исходники коммерческих библиотек никто раздавать, само собой, не будет ради этого.
0
Ничего не понял. Если либа возвращает растовую ссылку, то владелец этой ссылки должен быть не в либе, а в коде, использующем либу — он владельца и прибъет. Разве может быть ситуация, что из либы возвращается ссылка, и владелец этой ссылки сидит внутри либы? У меня компилятор ругается, что «объект не живет долго».
-4
:))) Да ужж, при таком подходе забавно было бы посмотреть на чисто растовский аналог обычной библиотечной функции для работы с памятью типа malloc(). Для библиотек вообще-то создавать некие свои ресурсы и возвращать на них указатели или хэндлы для дальнейшего использования в этой же библиотеке (а затем и освобождения) — это нормальная практика. Не всегда можно требовать, чтобы владелец ссылки в коде, использующем либу, вообще был в курсе, сколько памяти нужно выделить под соответствующий объект, например. Не его это собачье дело.

P.S. И, кстати, что такое «владелец»? В графе, например, или в двусвязном списке, кто чей владелец?

P.P.S. Растаман у меня (сорри, конечно, но не могу удержаться) ассоциируется с человеком в смирительной рубашке в кресле-каталке, который ходит ездит не туда, куда ему нужно, а туда, куда повезет санитар, но зато счастлив, что не может случайно упасть и ушибить коленку. При этом вероятность разбить коленку по факту все равно никуда не девается потому, что это в любой момент могут сделать многочисленные окружающие.
0
PsyHaSTe более компетентен в расте, насколько моих знаний хватает — любая функция может вернуть либо копию данных, либо указатель на память, выделенную до вызова этой функции, возможно есть исключения, не знаю.
Графы и списки — это ж известный траходром раста, там специальные контейнеры для этого запилили :)
0
любая функция может вернуть либо копию данных, либо указатель на память, выделенную до вызова этой функции, возможно есть исключения, не знаю.

Видимо, через пресловутый unsafe исключения и запиливаются.

Графы и списки — это ж известный траходром раста, там специальные контейнеры для этого запилили :)

Не удивлен вообще нисколько.
+1
Да ужж, при таком подходе забавно было бы посмотреть на чисто растовский аналог обычной библиотечной функции для работы с памятью типа malloc().

Да пожалуйста, смотрите


P.S. И, кстати, что такое «владелец»? В графе, например, или в двусвязном списке, кто чей владелец?

В графе нет владельца, нужно использовать shared_ptr. — Rc<T>, арены и прочее. Пример как можно сделать здесь.


P.P.S.

Пишите на жс, там вообще все разрешено.

-1
Да пожалуйста, смотрите

Да, я посмотрел:

pub(crate) fn alloc_pages(pages: Pages) -> Result<ptr::NonNull<u8>, AllocErr> {
    unsafe {
        let bytes: Bytes = pages.into();
        let addr = libc::mmap(
            ptr::null_mut(),
            bytes.0,
            libc::PROT_WRITE | libc::PROT_READ,
            libc::MAP_ANON | libc::MAP_PRIVATE,
            -1,
            0,
        );
        if addr == libc::MAP_FAILED {
            Err(AllocErr)
        } else {
            ptr::NonNull::new(addr as *mut u8).ok_or(AllocErr)
        }
    }
}

Не агритесь вы так, я ничего не имею против Раста жеж. Нравится людям на нем писать — очень хорошо. Просто вот люди, которые на полном серьезе пишут, что «компилятор все за нас проверит», «компилятор гарантирует» — это… ну… Шаг влево — шаг вправо, нестандартная задача, вызов сисколла, сторонняя библиотека без исходного кода (даже и написанная внутри на том же Расте), и все — «гарантии компилятора» превращаются в тыкву сразу. Раздражает не раст, раздражает его неумелая и грубая пропаганда.
0
Там еще столько TODO, что до завоевания мира расту еще далеко :)
0
У меня Redox не открывается дальше начальной страницы, может рокн-рол мертв, а мы еще нет?
0
Чертова цензура, у меня только через TOR, видимо там внутри сплошной прон…
0

При чем тут пропаганда, если это работает? Да, чтобы реализовать низкоуровневую работу с памятью нужно unsafe, что дальше?


С тем же успехом весь haskell компилируется в машинный код, значит ли это что мы потеряли все что нам давали типы?..


В общем, спор ни о чем.


Programming Defeatism: No technique will remove all bugs, so let's go with what worked in the 70s. ©


Не агритесь вы так

Когда мне начинают рассказывать про смирительные рубашки и невероятную свободу, которую он дает, я вспоминаю слова кармака

+1
Когда мне начинают рассказывать про смирительные рубашки и невероятную свободу, которую он дает, я вспоминаю слова кармака

Ну товарищ, которому он отвечал, тоже в чем-то прав :)
Game dev people discovering Rust is pure comedy. Tweeting out all the benefits, and I'm like «Yup, all the things you mentioned are also available in C++, and you've been ignoring and criticizing them for years». ¯\_(ツ)_/¯
0
Да, чтобы реализовать низкоуровневую работу с памятью нужно unsafe, что дальше?

Нужен не только unsafe, но и libc( в данном случае).
0
Ну он как бы unsafe полностью, все вот эти «компилятор гарантирует» на нем в пролете. А иначе никак.
0

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


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

-2
Да пожалуйста, смотрите

Проблема подобных ответов заключается в том, что в понятия «аналог» входит то, что является сравнимым хотя-бы по каким-то основным критериям. В данном случае основном критерия аллокатора — работать быстро. Этот аллокатор неконкурентоспособен не то что каким-то лучшим аллокатора, а даже самым банальным.

0
А чего вы минусуете? У вас есть какая-то контр-аргументация? Я что-то неправильно сказал?
+2

Почему нет? Все лайфтаймы и заимствование ссылок описаны в объявлении функции. Далее есть код по одну сторону интерфейса (клиент) и по другую (библитека). Им ничего не нужно знать про друг друга — они работают через интерфейс. Есть время жизни 'static для каких-то синглетонов библиотеке. Safe Раст не возвращает голый указатель. Если мы возвращаем ссылку из функции то должны объявить её время жизни, которое зависит от времени жизни входных параметров. Или мы можем передать владение объектом наверх.


Если мы имеем дело с dll которая на Cи то нужно писать обёртку через unsafe. Ну тут серебрянной пули не существует. Либо жить на си/плюсах либо писать обёртки либо переписывать всё на раст. Очевидно что в зависимтости от ситуации и количества кода то или иное решение оптимально.

+1

И? Это ведь УБ, там хоть ханойские башни могут запуститься.

0

На то оно и UB. Попробуйте количество элементов увеличить, или тип контейнера сменить.

0

Оно ещё и от запуска к запуску может меняться.
А может стабильно работать месяцами.
Если тестирование налажено хорошо на всех уровнях повезёт, то странность в обработке данных возможно заметить до продакшна.
Падение приложения вместо порчи важных данных можно считать большой удачей.

+1
Везде и всюду видно обратное

Раст целиком и полностью состоит из костылей

жава

ненужен


Судя по качеству аргументов перед вами же типичный лоровец, зачем вы с ним спорите?
+5

Ваш комментарий ниже куда-то делся, но я на него уже подготовил ответ...


Целиком и полностью. Зависит от libc, от libc и любых других аллокаторов написаных на си. Использует компилятор и его рантайм написанный на С/С++.

libc — это лишь абстракция, где "с" в названии — это лишь история. В Redox OS (ОС на Rust) используется relibc (реализованная по большей части на Rust).


В #[no_std] вообще без алокаторов живут (реалии встаиваемых систем). При необходимости можно алокатор свой написать и переопределить, только возникнет резонный вопрос: "а зачем?"


Использует компилятор и его рантайм написанный на С/С++.

Компилятор Rust написан на Rust. В дополнение (а там будет видно) к кодогенератору LLVM разрабатывают альтернативный кодогенератор на Rust — Cranelift (и всё-таки нет смысла сразу всё бросать и нестись всё переписывать на Rust, но планомерные качественные изменения очень даже происходят).


А пока есть одни хлеворды и серво, которое не может нарисовать хелворд без артефактов — меня эти рассуждения волнуют мало и волновать вообще не должны.

"Вот когда будет..."


Ждите дальше.


К тому же, раст состоит целиком и полностью из копирования. Там копируется всё и единственное, что ему позволяет существовать — это llvm и его оптимизации, которые выпиливают все эти копирования.

В Rust используется move-семантика по умолчанию. О каком компировании речь вообще?


Что, методичка сломалась и ответа нету?

Ответа к чему? Вопроса не звучало. Я готов увидеть Ваши патчи в компиляторы перечисленных языков и тогда будем обсуждать по делу. Исходники Rust — это единственные исходники крупного языка программирования, которые не вызывают у меня ужаса, даже более того, компилятор реализован как самый "обычный" проект на Rust и всё логично расположено и код написан с минимальным использованием магии, хорошо документирован и покрыт тестами.


Плохо это тем, что очередное пхп украв логику у llvm выдаёт достижения llvm за свои. Без это достижений — это бы пхп было пхп. А оно и было пхп.

По Вашей логике выходит, что LLVM справедливо использовать только для компиляторов С++.

0
Похоже, админы забанили пользователя по чьей-то жалобе. У меня принцип — никого не минусовать, так что жалко его, смешно писал, и кое-что даже по делу было. По мне так форма выражения мыслей неважна — хоть матом, лишь бы Мысль была.
PS
Самое смешное, что Swift тоже зависит от LLVM :)
0
Похоже, админы забанили пользователя по чьей-то жалобе.
у меня было желание пожаловаться когда он уже окончательно скатился, но я только комментарий написал о том, что при такой подаче искать что-либо в тексте желания не возникает. Но либо сами админы нашли, либо кто-то ещё отписался…
0

Тьфу ты, только я начал писать ответ на его комментарий недельной давности...

0
> В Rust используется move-семантика по умолчанию.
========
Тут путаница какая-то у меня в голове. Если я создю структуру внутри функции, а потом ее возвращаю вверх по стеку — какие есть варианты кроме копирования? Или в другой тред передаю. Получается, что компилятор сам определяет по ситуации — мув там или копи? Со ссылками то как раз все понятно, непонятно с передачей владения.
+3

Семантика — это модель, которая предоставляется программисту, в рамках которой он(а) может создавать описывать логику своей программы. Move-семантика — это про то, что после передачи владения значения из переменной А в переменную Б, обращение к переменной А не имеет смысла (в Rust — это ошибка компиляции, а в С++ — поведение не определено [UB]). С точки зрения реализации — это уже вопрос другой. Можно Move-семантику оставить моделью, а асемблер будет производить копии, но чаще всего оптимизационные компиляторы достаточно умны чтобы пользоваться этим преимуществом и избегать копий. Однако, магии не существует и на границах функций (если они не заинлайнятся), данные должны как-то через регистры и стек попасть к вызываемой функции, так что если другого выхода не будет найдено, то данные будут "копироваться".

0
в С++ — поведение не определено [UB]

Это не совсем так. Например, объекты классов из STL после перемещения являются «valid, but unspecified».
+1

Не знал, спасибо за уточнение. Скажу честно, на С++ мне писать код просто страшно. Я думаю мне всей жизни не хватит чтобы устранить моё невежество в области специфики С++.

+2

Что означает, что любые операции с ними, кроме вызова деструктора или оператора присваивания — UB.

+2

Что означает, что любые операции с ними, кроме вызова деструктора или оператора присваивания — Unspecified Behavior.


Undefined Behavior тут неоткуда возникнуть.

+2

Мы оба неправы.


Те операции, которые имеют предусловия — UB. Те, которые не имеют — unspecified.

0
Ну, то есть String копируется всегда, хотя по сути это указатель?
+1

В случае String ничего копироваться не будет, потому что это значение из кучи


https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=c78abc860e0e096c6f9aaeb909a92181


Будет копироваться только сам адрес указателя, который простой int, а все инты как мы знаем проще скопировать. Контент строки остается нетронутым.

0
Например, функция из библиотеки serde_json возвращает мне не String а &str. Я понимаю, что это аллоцировано где-то в куче, и владелец внутри библиотеки сидит. Но мне со ссылками неудобно, и я хочу из этого слайса сделать нормальный String. И вопрос — если я к этому &str применю to_string(), не приведет ли это к новому выделению памяти? По доке получается что приведет, а по жизни?
+6

Очень просто, serde-json ничего не аллоцирует. Функция не может вернуть &str на локальный объект, потому что это была бы висячая ссылка. Отсюда получаем очевидный факт, что все &str которые возвращает функция это просто слайсы, а владелец — строка, переданная в from_str. Зиро кост, аллокаций нет.

+2
То есть библиотека пробрасывает ссылки от входного слайса, полученного в from_slice() — на выход. Это очень круто, поэтому она такая быстрая.
0

Естественно приведёт. Но вопрос в том, какая именно функция возвращает &str. И нельзя ли эту строку "выковырять" из объекта. И надо ли вообще.

+3

Да нет, весь компилятор. Никто не говорит, что таргетом компилятора должен быть машинный код. Таргет csc например это MSIL.

0
Таргет csc например это MSIL.

Даже если бы это и имело практический смысл, в таком случае для запуска программы потребуется CLR, написанный на c/c++.
0

Какая разница? Надеюсь, вы не будете спорить, что csc (C Sharp Compiler) это компилятор?

0
если брать наиболее широкое из возможных определений слова «компилятор», то формально вы правы. Но с такой терминологией препроцессор си тоже можно назвать отдельным компилятором, а clang так вообще целой пачкой. Если же говорить о компиляторе как о программе, преобразующий исходный код в исполняемые файлы, вдруг окажется, что csc — не компилятор.
0

Спор об определениях мне не очень интересен. Майкрософт вряд ли планировал всех запутать или использовать маркетинговый трюк, чтобы все думали, что это компилятор, а на самом деле… Кстати, а что это тогда? Вроде не интерпретатор. А других вариантов и нет. Транспайлер частный случай компиляции, причем тут его правило одноуровневости языков не соблюдается, а на этом перечень исчерпан.

0

Ну вот в том-то и спор, считать ли байт-код .NET исполняемым. :) Это всё философские рассуждения, которые не очень-то и принципиальны на мой взгляд.

-1
я бы назвал «исполняемым» «исполняемый процессором» код, а всё остальное — интерпретируемым.
+1

Есть лисп-машины, с тем же успехом можно сделать процессор, выполняющий net код. Что дальше?


clang получается кстати тоже интерпретируемый?

0
вот когда появится такой процессор, тогда я поменяю свое мнение
0

Свойство языка не меняется от того, что там где-то какой-то процессор кто-то сделал.


Кстати да, интерпретатор это штука, которой на вход поступает некий исходный код, а на выходе она его собстенно интерпретирует, выполняя команды. csc ничего не выполняет, он просто кладет некий файлик с IL-кодом. Вопрос, как так?


И про clang ответьте, пожалуйста. Тоже интерпретатор?

-1
Программа имеет смысл только тогда, когда конкретное железо (target) способно её выполнить. И в конвеере от «исходного кода на языке Rust» до выполнения 99% кода написано на с/с++. Вы комфортно обособили написанную на расте часть и обозвали её компилятором. Вот только самостоятельно эта часть не способна обеспечить выполнение кода, а значит, рассматривать её можно лишь как часть (причем очень малую) инфраструктуры.

И про clang ответьте, пожалуйста. Тоже интерпретатор?

С чего бы ради то?
+1
С чего бы ради то?

Ну потому что clang делает ровно то же, что и растовый компилятор — собирает llvm из сишного кода.

-2
clang'ом обычно называют весь компилятор (набор утилит, обеспечивающих преобразование исходного кода в исполняемые файлы), включая llvm и линкер. Он написан в основном на с/с++. По аналогии, rustc написан… всё еще в основном на с/с++.
0
clang'ом обычно называют весь компилятор (набор утилит, обеспечивающих преобразование исходного кода в исполняемые файлы), включая llvm и линкер

llvm это llvm. Линкер же и в расте есть.


По аналогии, rustc написан… всё еще в основном на с/с++.

Раст вообще никогда не был написан на С++. Раст написан на самом расте, и отбутстраплен с OCaml, а никак не с плюсов.

-1
llvm это llvm. Линкер же и в расте есть.

Откуда же в rustc линкер, если у него нет бинарных таргетов? линкера в rustc нет. Линкером там является llvm.

+2
Ну, если придираться, то в llvm нет компоновщика. Компоновщик lld — отдельный проект в рамках llvm, как clang или lldb.
Скомпилированные clang объектные файлы можно скомпоновать в бинарник и ld, и lld, и другими компоновщиками. Rust по умолчанию пользуется lld, но можно компоновать и ld.
-1
Компоновщик lld — отдельный проект в рамках llvm, как clang или lldb.


в рамках llvm
в llvm нет компоновщика.

Это явные противоречия. Да и попросту глупость, т.к. все проекты в рамках llvm зависят от llvm и являются его частью.

Rust по умолчанию пользуется lld, но можно компоновать и ld.

И что же из этого следует? В rustc появился линкер на rust? Нет.
-4
Давайте тогда другой пример. Вот есть у меня исходный код программы на языке rust. Могу ли я запуская только компилятор(ы), написанный(е) на rust, получить из него нативный код процессора? Нет. Можно ли это сделать запуская компилятор(ы), написанный(е) в т.ч. и на с++? Да. Сколько кода такого комплекта написано на rust? Максимум пара процентов. Можно ли считать компилятор, в котором пара процентов кода написана на языке A, написанной только на языке A? Точно нет. Но это противоречит изначальному утверждению:
Компилятор Rust написан на Rust

Вывод: изначальное утверждение неверно.
0

Хорошо, давайте так. Компилятор раста — компилятор rust кода в байткод LLVM IR. Может ли он его сгенерировать (используя АПИ lLVM или еще как-то)? Может.


А нативный или нет уже вопрос конкретного процессора. Для ARM ваш x86 плюсовый код не будет нативным, для итаниума тоже не будет. Да и для интела он не совсем нативный, если вспомнить как CISC команды в наше время работают. Вопрос, на какую глубину мы смотрим, вот и все.


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

0
А нативный или нет уже вопрос конкретного процессора

ну так мы обычно отталкиваемся от конкретного процессора (таргета), а потом смотрим, какой код будет для него нативным. И какой бы из существующих процессоров мы ни взяли, ни для одного из них байткод LLVM IR не будет нативным.

С другой стороны можно сделать процессор, который нативно выполняет LLVM

теоретически можно, практически — это не нужно, ибо сложно и бесполезно. Я всё-таки говорю о чисто практическом аспекте.
0

Ну а мы все же смотрим больше с точки зрения теории. А с точки зрения теории LLVM от x86 особо не отличается, и компиляторы и в то, и в другое в целом очень похожи, имеют одни и те же модули, и т.п.

0
Ну а мы все же смотрим больше с точки зрения теории

Кто «мы» то? Я вот буквально сообщением выше же написал:
Я всё-таки говорю о чисто практическом аспекте.

И с самого начала спора я дал недвусмысленно понять, что меня интересует именно практический аспект вопроса:
Даже если бы это и имело практический смысл...

Да и в других сообщениях мой посыл прослеживался… Почему вы постоянно спорите не читая написанное собеседником?
0
меня интересует именно практический аспект вопроса

С практической точки зрения никто не будет переписывать то, что и так работает, только чтобы победить в интернет споре.

0
С практической точки зрения никто не будет переписывать то, что и так работает, только чтобы победить в интернет споре.

ну я про это и говорю. Использовать по максимуму инфраструктуру LLVM разумно, спору нет. Но вот утверждать что компилятор раста, её использующий, написан только на rust — вот это прям перебор.
0
И про clang ответьте, пожалуйста. Тоже интерпретатор?

Конечно)):


constexpr int foo() {
    return 2*2;
}
-2
Есть лисп-машины

Нету. лисп-машина — это интерпретатор лиспа. Причём даже не хардварный. Об этом даже в википедии написано. Зачем люди продолжают повторять эту глупость(что лисп-машины исполняли какой-то лисп)?

Так же, упускается из виду фундаментальное обстоятельство. Никому и никогда ненужно просто исполнение. Нужно эффективное исполнение. И именно этим лисп-машины и были. Просто хардварные оптимизации характерные для lisp. Такая же история есть с java-байткодом в каких-нибудь arm.

Все эти ухищрения неэффективны. Именно поэтому они и умерли, а существовали только на начальных этапах. Как в том же arm. Исполнитель откровенно слаб, частота маленькая. Проще туда впихнуть побольше всякой логики и будет быстрее. Но чем дальше — тем хуже.

Именно поэтому и умер циск. Когда-то это казалось хорошей идеей, а сейчас воспринимается за глупость.

+1
Так же, упускается из виду фундаментальное обстоятельство. Никому и никогда ненужно просто исполнение. Нужно эффективное исполнение.

Вам кажется. В качестве контраргумента достаточно посмотреть на засилие электрона вокруг, которму до эффективности как до луны. Однако бизнесу он очень нравитяс. Как же так?

0
Вам кажется.

Нет.
В качестве контраргумента достаточно посмотреть на засилие электрона вокруг,

В качестве контр-аргумента подойдёт и rust. Но это не является контр-аргументом, т.к. и одно и второе не является исполнителем и не является хардварным.

которму до эффективности как до луны.

Даже если я приму эти попытки, то они в любом случае неудачны. Исполнителем в случае с электроном является хром. Хром является самым эффективным броузером, v8 является самым эффективным исполнителем js.

0
Нет.

Да.


В качестве контр-аргумента подойдёт и rust. Но это не является контр-аргументом, т.к. и одно и второе не является исполнителем и не является хардварным.

Ну ок, допустим не является. Дальше что? Какие из этого практические выводы полезные сделать можно?


Даже если я приму эти попытки, то они в любом случае неудачны. Исполнителем в случае с электроном является хром. Хром является самым эффективным броузером, v8 является самым эффективным исполнителем js.

От того что v8 является эффективным софтом на плюсах электрон не стал эффективным. Можно посмотреть на слак который гигабайты отжирает там, где телеграм обходится десятками мегабайт. А то можно дойти до того, что "весь код выполняется на процессоре, вылизанным десятками гениальных инженеров интела, значит весь код эффективный".

-2
Ну ок, допустим не является. Дальше что? Какие из этого практические выводы полезные сделать можно?

Выводы я сделал выше. Я опроверг ваши наивные рассуждения про lisp-машины. Я опроверг ваши рассуждения про «можно сделать какой угодно исполнитель». Нельзя. Этим никто не будет заниматься. Это практический вывод.

От того что v8 является эффективным софтом на плюсах электрон не стал эффективным.

Из этого ничего не следует. Разговор был о хардварных исполнителях. Я вам дал возможность подменить тему на софтварные исполнители. В очередной раз подменить тему я вам не дам.

Можно посмотреть на слак который гигабайты отжирает там, где телеграм обходится десятками мегабайт.

Какое это имеет отношение к теме? Мы не обсуждали исполняемый код, а обсуждали исполнитель.

Даже больше скажу. Я утверждал(и именно об этом я говорил), что исполнитель под «неэффективно» нельзя сделать эффективно. Это одно из следствий. И даже непросто «сделать эффективно» просто «сделать» нельзя.

А то можно дойти до того, что «весь код выполняется на процессоре, вылизанным десятками гениальных инженеров интела, значит весь код эффективный».

Зачем это написано — я не знаю.

0
Выводы я сделал выше. Я опроверг ваши наивные рассуждения про lisp-машины. Я опроверг ваши рассуждения про «можно сделать какой угодно исполнитель». Нельзя. Этим никто не будет заниматься. Это практический вывод.

Я раст выполняю нативно, очевидный контраргумент.


Остальное комментировать не буду, а то опять пожалуетесь на другую тему.

-3
Я раст выполняю нативно, очевидный контраргумент.

Нет. Раст не выполняется нативно — нативном выполняется нативный код. Для того и нужен компилятор.

Остальное комментировать не буду, а то опять пожалуетесь на другую тему.

Там уже нечего комментировать. Ваша аргументация зашла в тупик.
0
Нет. Раст не выполняется нативно — нативном выполняется нативный код. Для того и нужен компилятор.

Ну вот я написал код на расте, получил нативный исполняемый файл. Могу его запустить где угодно? Могу. Так в чем трабл?


Там уже нечего комментировать. Ваша аргументация зашла в тупик.

Вот я пошел вам на встречу, а вы вот так. Некрасиво, однако )

-2
Ну вот я написал код на расте, получил нативный исполняемый файл.

получил нативный исполняемый файл

На этом этапе раста уже нет.

Вот я пошел вам на встречу, а вы вот так. Некрасиво, однако )

Это я пошел вам на встречу, а мог бы закончить на факапе с lisp-машинами.

0
На этом этапе раста уже нет.

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


Это я пошел вам на встречу, а мог бы закончить на факапе с lisp-машинами.

Последний раз я игнорирую ваши наезды.

-2
Допустим. Какой из этого вывод? Какая польза от этого утверждения? На этом этапе вообще никаких языков нет, ни сипипи, ни хаскеля.

Очень простой вывод.

Изначальный ваш тезис:
Есть лисп-машины, с тем же успехом можно сделать процессор, выполняющий net код. Что дальше?

Я, конечно же, опроверг существование лисп-машин(в том виде, в котором декларировали их вы). Опроверг состоятельность попыток реализовать процессор выполняющий lisp/.net и вообще чего угодно. Мир не знает подобных примеров, а все попытки расширять даже архитектуру(привет циск) потерпели неудачу.

Я подтвердил это:

я бы назвал «исполняемым» «исполняемый процессором» код, а всё остальное — интерпретируемым.

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

Последний раз я игнорирую ваши наезды.

В чём наезд?

0
Я, конечно же, опроверг существование лисп-машин(в том виде, в котором декларировали их вы). Опроверг состоятельность попыток реализовать процессор выполняющий lisp/.net и вообще чего угодно. Мир не знает подобных примеров, а все попытки расширять даже архитектуру(привет циск) потерпели неудачу.

aJile: http://www.ajile.com/
Imsys: http://www.imsystech.com/
JOP: http://www.jopdesign.com/index.jsp


Вот примеры ЦПУ способных выполнять JVM байткод напрямую, для которых Java — нативный язык. Так что не знаю, что вы там опровергли. Получается Java — нативный язык.

-3
Опять попытка смерить тему, но неудачная. Про жава-байткод я уже говорил:

Просто хардварные оптимизации характерные для lisp. Такая же история есть с java-байткодом в каких-нибудь arm.

Так же я там говорил и про причины их появления. Все эти ссылки из википедии — целиком и полностью ложатся на моё описание.

По поводу самого «опровержения».
JVM байткод напрямую, для которых Java — нативный язык.

Это полная чушь. java и jvm-байткод это разные вещи. Далее видна попытка дёргать фразы из контекста, хотя я уточнял:

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

jvm-байткод не является java, не является высокоуровневым языком. Так же требует компилятора/транслятора.

Так же, такого понятия как java вообще не существует. Существует много версий всяких разных java. И та java, которая используется в embedded — это совершенно другая жава(https://en.wikipedia.org/wiki/Java_Platform,_Micro_Edition).

Про эти акселераторы байткода я уже писал. Это существует только на уровне примитивной логики и ничего не умеет. Они не обладают какой-либо производительностью. Они не могут исполнять обычный jvm-байткод.

Получается Java — нативный язык.

Нет, и это очевидно.

0

С++ — нативный язык, потому что генерирует x86, исполняемый нативно
Java — нативный язык, потому что генерирует JVM, исполняемый нативно.


Вы уж либо крестик, либо еще что.

-2
Java — нативный язык, потому что генерирует JVM, исполняемый нативно.

Неверно. Нельзя генерировать jvm, да если имеет ввиду байткод, то байткод никто исполнять не умеет. Я уже говорил, что это embedded java, а не обычная Java. И она целиком и полностью несостоятельна для решения прикладных задач.

С++ — нативный язык, потому что генерирует x86, исполняемый нативно

С++ не может исполняться. Именно об этом говорилось. Подобная формулировка вообще не имеет смысла, т.к. любой язык так или иначе преобразуется в нативный код. И именно нативный код исполняется.

Под нативным в контексте С++ имеется ввиду не то, что он преобразуется в нативный код, а в том что он исполняется нативном на платформе. И противопоставляется это языкам, которые требуют эмуляции платформы. Это называется vm. Именно поэтому то, что исполняется в jvm по определению не может является нативным платформе.

Так же, все эти рассуждения вообще не имеют смысла т.к. никаких хардварных исполнителей байткода не существует. Всё что существует — не является системами/языками общего назначения и вообще находится вне контекста. На этом уже можно закончить.

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

Вы ссылки выше прочитайте. Вон процессоры, выполняющие байткод.


С++ не может исполняться. Именно об этом говорилось.

А я этого не говорил. Перечитайте фразу "генерирует х86, исполняемый нативно"

-2
Вы ссылки выше прочитайте. Вон процессоры, выполняющие байткод.

Ещё раз, байткод не жава. Зачем я повторю одно и тоже, одно и тоже тому, что попросту всё неудобное игнорирует и повторяет одни и те же фразы, на которые же был дан ответ.

А я этого не говорил. Перечитайте фразу «генерирует х86, исполняемый нативно»

Ложь.

Есть лисп-машины, с тем же успехом можно сделать процессор, выполняющий net код. Что дальше?

Говорилось именно про высокоуровневые языки и их байткод(хотя причём тут тогда лисп-машины?).

Если же говорить о компиляторе как о программе, преобразующий исходный код в исполняемые файлы, вдруг окажется, что csc — не компилятор.


Т.е. вы уже сами себя опровергли, т.к. доказали изначальный тезис автора. Что для того, что-бы java-байткод был нативным — должен быть нативный его исполнитель. Очевидно, что вы тут тоже пытаетесь подменить понятия, т.к. javame не является java.

Мой же тезис заключался в том, что 1) нативные реализации байткод/лист-акселераторов существуют. Я это сказал изначально. 2) они ненужны и несостоятельны в общем контексте. Это всё применяется на слабом железе, причём применяется редко и никак вверх не скалируется. Именно поэтому этого никогда за пределами примитивной логики не будет.

К тому же, зачастую во всех эти байткод-акселераторах применяется обычная бинарная трансляция. Т.е. реальная архитектура железки иная. Она не исполняет напрямую байткод, а просто транслирует байткод в некий набор обычных инструкций.

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

Ничего из этого вы не опровергли. Вы начала мне кидать ссылки из википедии. Абсолютно непонятно зачем, ведь я ещё до вас вам сообщил, что подобные решения есть.

0
Ещё раз, байткод не жава. Зачем я повторю одно и тоже, одно и тоже тому, что попросту всё неудобное игнорирует и повторяет одни и те же фразы, на которые же был дан ответ.

Javac выдает байткод из Java-исходника. Что в этом утверждении вам непонятно?


Ложь.

В каком месте? С++ не генерирут х86? Или х86 это не нативный код?


Говорилось именно про высокоуровневые языки и их байткод(хотя причём тут тогда лисп-машины?).

байткод джавы достаточно высокоуровневый. На одном уровне с LLVM или MSIL.


Т.е. вы уже сами себя опровергли, т.к. доказали изначальный тезис автора. Что для того, что-бы java-байткод был нативным — должен быть нативный его исполнитель. Очевидно, что вы тут тоже пытаетесь подменить понятия, т.к. javame не является java

Я выше скинул процессоры, для которых байткод — нативный формат.

0
Хорошо.
Есть лисп-машины

Покажите мне машину, которая исполняет лисп.
0

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

-2
Окей, допустим про лисп вы меня уговорили.

Со всем остальным будет тоже самое. Так было с машинами, ir и прочими аллокаторами.

Вот примеры ЦПУ способных выполнять JVM байткод напрямую,

Нет. Но мы пока опустим это. Начнём с вопроса простого:

Такая же история есть с java-байткодом в каких-нибудь arm.

Все эти ухищрения неэффективны. Именно поэтому они и умерли, а существовали только на начальных этапах. Как в том же arm. Исполнитель откровенно слаб, частота маленькая. Проще туда впихнуть побольше всякой логики и будет быстрее. Но чем дальше — тем хуже.


На каком основании вы мне 1) показываете как контр-аргумент моим утверждения то, о чём я заранее и до вас сказал. 2) почему были проигнорированы мои объяснения по этому поводу?

К тому же я не вижу от вас доказательств, что эти накиданные вами ссылки из википедии действительно исполняют jvm-байткод(весь) и целиком и полностью имплементируют jvm хардварно.
0

А в данном вопросе меня не интересует вопрос эффективности, а только вопрос возможности. Ну примерно как факт, что в SystemF возможен фуллинференс. Не очень часто на практике используется, но для теоретических построений очень важный.


Какая спека байткода и в каком объеме реализована можете посмотреть по ссылкам, что я дал. Копаться там чтобы вам что-то доказать мне не хочется. Хотите убедиться — убеждайтесь.

-4
Никто rustc не декларирует как транслятор из rust в llvm-ir. Мало того, что это попросту глупо. Причин много. Начиная из того, что ir уже llvm, его внутренние представление. И это именно задача фронтенда генерировать промежуточное представление компилятора. Получается, что компилятор компилирует то, что потом компилирует компилятор. Какой смысл вообще разделять фронтенд и компилятор, если и то и компилятор? Заканчивая тем, что на уровне языка множество ссылок на именно генерацию программ. Те же модули существуют именно на бинарном уровне.

Даже если всё это исключить и принять эту логику, то это ничего не изменит. Генератор ir в rustc зависим от llvm. Именно llvm генерирует ir, а фронтенд использует api для генерации ir. Таким образом те части, которая в rustc написаны на rust сами генерировать ir не могут.

Единственная соломинка здесь — это mir. Но это тоже тупиковый путь. Это некая форма rust. Её ничто не умеет исполнять. Она декларируется как промежуточное представление. Это не таргет. Если это таргет, то зачем компилятору что-то делать с таргетом и куда-то его преобразовывать(пусть и внешней логикой)?

0
Единственная соломинка здесь — это mir. Но это тоже тупиковый путь. Это некая форма rust. Её ничто не умеет исполнять. Она декларируется как промежуточное представление. Это не таргет. Если это таргет, то зачем компилятору что-то делать с таргетом и куда-то его преобразовывать(пусть и внешней логикой)?

Если бы вы почитали, зачем MIR вообще придумали, то увидели бы, что это сделано для более простых и глобальных оптимизаций и некоторых других вещей. Что насчет "внутренних представлений", то етсь такая штука, как абстракция, и они как матрешки нанизываюстя друг на друга. Есть HAL, который абстрагирует ОС, которую абстрагирует clib, которую абстрагирует фреймворк, который абстрагирует...

-2
Если бы вы почитали, зачем MIR вообще придумали, то увидели бы, что это сделано для более простых и глобальных оптимизаций и некоторых других вещей.

Пруфы. К тому же, что вообще из этого следует? Я опроверг ваши рассуждения. Абсолютно неважно то, чем является mir. Таргетом в rustc может является только он, ведь только его генерирует rustc сам.

Что насчет «внутренних представлений», то етсь такая штука, как абстракция, и они как матрешки нанизываюстя друг на друга.

Это набор слов. Узнайте значение понятия «таргет». Внутреннее представление, либо промежуточное представление — это по определению то, что находится до таргета. Если бы оно было таргетом — оно бы не называлось промежуточным/внутренним.

Есть HAL, который абстрагирует ОС, которую абстрагирует clib, которую абстрагирует фреймворк, который абстрагирует...

И? Я вас где-то спрашивал про «что такое промежуточное представление»? Я вас где-то спрашивал про «что такое абстракции»? Нет и нет. Зачем вы мне это пишите?

Вы пытались сказать, что в rustc rust-код генерирует ir. Это неправда. Вам нужно на это отвечать.
0

rustc генерирует LLVM IR, который является IR. В процессе этой генерации он внутри себя проходит через промежуточное представление MIR.


И да, хамить в комментах последнее дело.

-4
rustc генерирует LLVM IR

Нет. Контекст был «на rust». rustc не генерирует ir — его генерирует llvm.

В процессе этой генерации он внутри себя проходит через промежуточное представление MIR.

Я уже это говорил. mir — это единственное, что генерирует rustc сам. Вы утверждали, что rustc является компилятором(транслятором) во что-то там. Вы не уточняли во что именно он транслируется(понятно что в ir).

И вот, когда вы знаете, что rustc(сам) генерирует только mir. Таким образом отсюда:

Да нет, весь компилятор. Никто не говорит, что таргетом компилятора должен быть машинный код.

Может следовать только «таргет == mir». Почему mir не может являться и таргетом и промежуточным представлениям понятно.

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

И да, хамить в комментах последнее дело.

Конкретика. Где «хамить» и в чём «хамить» заключается.

0
Нет. Контекст был «на rust». rustc не генерирует ir — его генерирует llvm.

А что он тогда генерирует?


Я уже это говорил. mir — это единственное, что генерирует rustc сам. Вы утверждали, что rustc является компилятором(транслятором) во что-то там. Вы не уточняли во что именно он транслируется(понятно что в ir).

То есть LLVM понимает MIR, раз уж раст после него ничего не генерирует? Вот так новость.

-2
А что он тогда генерирует?

Только mir.

То есть LLVM понимает MIR, раз уж раст после него ничего не генерирует? Вот так новость.

Эту глупость, а не новость. Я уже всё объяснял. llvm предоставляет api для генерации ir. Если мы берём те части, которые в rustc написаны на rust(а именно об этом шла речь), то они никак не генерируют ir. Они используют хайлевел llvm-api через биндинги.

Повторю ещё раз. Те части, которые в rustc написаны на rust могут генерировать ТОЛЬКО mir. Из этого факта никак не следует, что llvm должен начать понимать mir.
0

А как из MIR получается исполняемый x86 файл? LLVM не понимает MIR, сам раст из MIR ничего не генерирует. ПО идее мы должны были бы застрять на этапе MIR.


image


Вот кто конкретно этот LLVM IR генерирует?

-2
Вот кто конкретно этот LLVM IR генерирует?

llvm. Я уже отвечал на это. llvm-ir генерирует llvm-логика, которая написана не на расте. Далее эту внешнюю логику раст использует через биндинги. Так работает любой llvm-фронтенд.

Эти рассуждения вообще не имеют смысла, ведь rustc попросту слинкован с llvm и без него в принципе существовать не может. Ваше утверждение изначально ложно. Но я пошел вам навстречу и говорю о том, что может(гипотетически) rustc в отрыве от llvm. Может он только генерировать mir(возможно ещё какие-то подобные промежуточные представления).

Компилятор Rust написан на Rust

Начальный тезис был, все рассуждения ведутся в этом контексте. Вы так же на это явно указывали.

Я думаю вы знаете, что из одного языка можно вызвать логику на другом языке. Именно таким механизмом пользуется rust.
0
llvm. Я уже отвечал на это. llvm-ir генерирует llvm-логика, которая написана не на расте. Далее эту внешнюю логику раст использует через биндинги. Так работает любой llvm-фронтенд.

Давайте я вам скину MIR, а вы с помощью LLVM попробуете из него получить LLVM IR? Спойлер, у вас не выйдет, потому что это просто не так.

0
Давайте я вам скину MIR, а вы с помощью LLVM попробуете из него получить LLVM IR? Спойлер, у вас не выйдет, потому что это просто не так.

Давайте мы поступим проще. Вы берёте rustc без llvm и генерируете ir. Если ваши рассуждения верны — вы сможете это сделать, ведь вы утверждали, что «rustc САМ(т.е. средствами кода написанного на rust) генерирует что-то там(как я понял — это IR)». Если сам — сгенерируйте.

К тому же, зачем вы продолжаете повторять глупости про mir и игнорируете все мои контр-аргументы? Из того, что llvm не умеет mir не следует, что rustc умеет генерировать(сам) ir. Это очевидно.
-1

Ну вон, пожалуйста, безо всякого LLVM сгенерил его.


Еще раз, раст это преобразователь Rust -> LLVM IR. LLVM дальше компилирует LLVM IR в таргет код платформы. Естественно именно задача раста сгенерировать вход для LLVM, коим является LLVM IR. В этом и смысл всей технологии. Тем же самым занимается clang, который тоже является компилятором. И не потому, что он компилирует си.

0
Ну вон, пожалуйста, безо всякого LLVM сгенерил его.

Покажите. Где? То, что вы там линкуете — генерирует rustc, который слинкован с llvm. Вам нужно убрать оттуда llvm и сгенерировать ir.

Еще раз, раст это преобразователь Rust -> LLVM IR.

Нет. Это глупость.

Естественно именно задача раста сгенерировать вход для LLVM, коим является LLVM IR.

Неверно. Никакой раст никакой ir не генерирует. раст занимается вызовов llvm-api, который генерирует ir. Именно llvm генерирует ir.

Я вам советую изучить тему перед тем как что-то утверждать. Начините отсюда github.com/rust-lang/rust/tree/master/src/rustllvm Далее github.com/rust-lang/rust/tree/master/src/librustc_codegen_llvm который напрямую забинден с rustllvm.

В этом и смысл всей технологии.

Нет, вы не изучали вопрос. Изучите.

IR является платформо-зависимым во многих аспектах. Он достаточно сложен. Именно поэтому никакой фронт его не генерирует.

llvm предоставляет api, который и занимается генерацией ir. Именно этот api использует rustc и любой другой фронт. Я даже больше скажу. Сам llvm знает про раст. Без поддержки llvm никакой фронт существовать не может.

Тем же самым занимается clang, который тоже является компилятором. И не потому, что он компилирует си.

Зачем вы ссылаетесь на clang? К чему это? Вы утверждали, что код на rust сам генерирует ir. Это неправда. В ситуации с clang это не работает. Да, тут так же ir генерирует llvm, но llvm написан на С++, а значит С++ генерирует для себя ir. И неважно где этот код лежит.

К тому же, clang это неотъемлемая часть llvm. rustc же внешняя часть.
-1
Зачем вы ссылаетесь на clang? К чему это? Вы утверждали, что код на rust сам генерирует ir. Это неправда. В ситуации с clang это не работает. Да, тут так же ir генерирует llvm, но llvm написан на С++, а значит С++ генерирует для себя ir. И неважно где этот код лежит.

Ну допустим. Какой вывод с этого? Вам так приятно, что софтина на плюсах написана? Раст изначально написан на окамле, должно ли окамловцам быть приятно? Должны ли они себя бить в грудь, что раст-то говорят не настоящий?

0
Ну допустим. Какой вывод с этого?

Не допустим. Это так и есть. Это мною доказано, как и доказано враньё с вашей стороны.

Вывод из этого очень просто, вы врали(ошибались) когда говорили, что rustc каким-то образом сам генерирует ir. С этим тезис я спорил. Его я опроверг.

Вам так приятно, что софтина на плюсах написана?

Ну почему же. Мне это неприятно, наоборот. Но в основном мне неприятно вранье и клоунада. Уровень и методику спора своих оппонентов я знаю. И доказать мне это не составляет труда.

Именно поэтому я могу доказывать, я могу выигрывать. Вы можете врать и гадить в карму/минусовать комменты.

Я даже не знаю зачем я начал спор с вами. Это глупо. В конечном итоге мне тут смог оппонировать только один человек. Радует, что хоть один есть. Посоветовать могу только одно. Больше знать/уметь и меньше заниматься спорами без знаний/умения.

Раст изначально написан на окамле,

Никакой раст не был написан на окамле. Это глупость. То, что оно называлось «rust» это не значит, что тот «rust» был сегодняшним rust.

должно ли окамловцам быть приятно?

Да нет. Наоборот. Rust тогда ничего из себя не представлял. А вот когда ушел к llvm. Но уже совершенно другая история.

Должны ли они себя бить в грудь, что раст-то говорят не настоящий?

Я не знаю кто что должен. Я знаю, что врать/утверждать не зная — плохо. Вы начали рассказывать всем, что rust может генерировать ir. Это ложь. Это я знаю. Остальное мне не интересно.

0
Вывод из этого очень просто, вы врали(ошибались) когда говорили, что rustc каким-то образом сам генерирует ir. С этим тезис я спорил. Его я опроверг.

Хорошо, а есть ли какой-то способ генерировать IR не через апи? Потому что если нет, то это демагогия в стиле "это пули убивают людей, а снайпер не при чем".


Да нет. Наоборот. Rust тогда ничего из себя не представлял. А вот когда ушел к llvm. Но уже совершенно другая история.

Емнип раст с самого начала компилился в llvm. Просто первая версия была написана не на расте, очевидно.




Энивей, я думал ллвм немного иначе работает. Спасибо за информацию.

-1
Вот кто конкретно этот LLVM IR генерирует?

Очевидно, что этот LLVM IR генерирует сам llvm. Очевидно для всех, кто работал с llvm.

+1
C++ проиграет, если на Rust писать чисто.

Вопрос. Проиграл ли С++?

ни один программист C++ в здравом уме не решится и будет копировать строки.

Я не решился. Копировал строки. С++ не проиграл. Как вы это объясните?
+1
В первую очередь тут не так допущение, что в свифте нет сборщика мусора. Он есть и зовётся автоматическим подсчётом ссылок или ARC. Как показывает теория и практика это один из самых медленных видов сборки мусора. Не так давно была любопытная работа о реализации юзерлевел драйвера сетевой карты. Свифт проиграл всем — си, расту, сишарпу, гоу. Чрезвычайно сильно проиграл. Как раз из-за подсчёта ссылок. Последние два при этом были недалеко от раста что по скорости, что по латентности. Сишарп только посильнее проигрывал
0
Спасибо. Я почему заинтересовался — в Rust ведь тоже активно пользуют счетчики ссылок Rc и Arc, особенно в многопоточном программировании с разделяемым состоянием. Теперь буду избегать счетчиков всеми силами :)
+5

В Rust их в очень многих случаях можно избежать. В многопоточке они используются для целей шаринга состояния. Обычно это одна копия на задачу, что, как правило, почти незаметно. А вот в Swift, насколько я понимаю, любое ссылочное значение это Arc<_>.

0
Счетчики разные бывают и есть подозрение, что причина тормозов в их универсальности в свифт. Там же сразу сделано с прицелом на потокобезопаность, а значит куча атомарных операций. Если посмотреть на swift_retain и swift_release, то там найдется довольно нетривиальный код даже в fast-path с атомарными инструкциями и циклами. Это при том, что еще есть slow-path. И такие вызовы компилятор пачками повсюду вставляет везде, где хоть как-то фигурируют объекты, покрываемые ARC. Да еще и не инлайнится это все что, как правило, делают в более продвинутых сборщиках мусора, где есть барьеры.

В С++ shared_ptr тоже не жалуют по той же причине — он медленный.

В Rust все таки философия такая, что правила владения максимально исключают необходимость в ручном управлении памятью, а счетчики вообще как крайний вариант используются.
0
Класс. Ну, сравнение строк и хеширование не отменить, а про приведение типов я догадывался. Непонятно правда, как работать с Any иначе, чем описано в документации is? / as?, это ж общее место для всех языков — Object в Java, interface{} в Go, возможно это грабли конкретной реализации Any в конкретном Swift, я-то этот язык толком не знаю.
0
Ну да, но подсчет ссылок подкосил результаты тоже прилично. Если копнуть глубже, то везде в профайле фигурируют вызовы рантайма с довольно приличными паузами. Еще хуже в самой процедуре хэширования — почти половину ее времени выполнения занимают манипуляции со ссылками, которые уходят аж в CoreFoundation. Тоже самое со сравнением — на манипуляции райнтайма уходят секунды. Хотя тут стоит признать, что если бы все бриджи до Foundation и CoreFoundation убрать, то эти бы секунды скорее всего исчезли.
0
Это для какой версии macOS и Swift? У меня вот так (https://habrastorage.org/webt/tk/4e/ji/tk4ejimhubxqfrilanvyrje_rlm.png), правда я исправлял несколько проблем.
0
Ну понятно, добрый дядя реализовал этот метод. Но хочется поддержки языка. Были обсуждения сделать опции енама отдельными типами, но статус сего непонятен.
0

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

+1

В языке есть поддержка. Варианты без значений можно сравнивать как обычно.


if x == None { /* ... */ }

Варианты со значениями — только через if let Some(_) = x. Просто потому, что здесь уже нужно решать, что мы делаем с запакованным значением: перемещаем, ссылаемся, просто игнорируем. В этом случае отдельный метод if x.is_some() вполне адекватен. Тем более, что его можно использовать в каком-нибудь функциональном контексте вроде it.filter(Option::is_some) или it.filter(Result::is_ok).


Конечно, в части случаев может показаться удобным писать в духе if x is Some. Но это настолько частный случай… Ведь декомпозиция это очень мощный инструмент, здесь и более сложные выражения можно использовать, например: if let Some(Err(_)) = x. Тут над адекватным синтаксисом для is уже стоит хорошо подумать.


А надо ли оно, что-то другое, когда есть уже универсальная декомпозиция, общая и для присваивания, и для матчинга, и для аргументов функций, у которой if let — это лишь частный случай распаковки не всех вариантов?


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

0
Варианты со значениями — только через if let

Прошу прощения, здесь стоит уточнить: "если мы не ожидаем какое-то конкретное значение".
if x == Some(42) и более сложные — точно так же прекрасно работают.

+3
Бинарник получился не маленким — 1,9 Мб, зато он один (про тупой линковщик Rust известно давно, обещaют починить).

Запускали с lto=true? Дебажные символы оставляли? Все это дает размер. Подробнее здесь.


Опциональный тип данных Swift (а также оператор приведения типов) сделаны синтаксически более изящно, через постфиксы ?! — в отличие от неуклюжего растового unwrap()

В коде не должно быть unwrap, если только вы не знаете чего-то, чего не знает компилятор, например, что значение всегда есть. Да и вообще в программе встречается 2 раза, и хорошо, что видно, где программа может упасть.


В Rust встречаются кривые синтаксические конструкции, например если нужно проверить значение JSON на пустоту, приходится писать один из 2-х смешных бредов:



хотя напрашиваются очевидные варианты:

Зачем плодить сахар на ровном месте? Зачем писать if myVal is Null если if let Null = myVal именно это и делает? Два одинаковых способа в языке делать одно и то же обычно не поощряется, особенно когда разница в одно ключевое слово.




Попробую в свободное время покрутить реализацию, вам рекомендую добавить по крайней мере lto=true и сравнить еще разок.

0
Спасибо, со всем согласен, но как сказать расту, что я хочу сделать проверку на НЕ-НАЛ?
if !(let Null = myVal)
или
if let Null != myVal
возможно, конечно, что это единственные грабли, или я чего не знаю.
0

Как я в другом комментарии написал, можно таки полноценно сравнивать в духе if my_val != None с вариантами без значений. Плюс всегда есть else, хотя он плох по двум причинам. С одной стороны — действительно не так красиво, и хорошо, что есть альтернатива. С другой — если там какое-то запакованное значение, его очень редко нужно игнорировать, поэтому знание о том, что "здесь не вариант А" — мало/редко полезно. Более того, и это самое важное, открытое множество вариантов перечисления — это антипаттерн. Лучше всего обрабатывать все варианты явно, а не полагаться на неизменность (ну, допустим, с некоторыми вроде Option, Result, Either мы себе это можем позволить, потому что они точно никогда не поменяются, но это скорее счастливые исключения). Перечисление может отрефакториться (добавятся варианты) и логика "открытого множества" может неявно поломаться. При полном перечислении вариантов в образце — компилятор нам просто сообщит, что какой-то вариант не обработан, и это — ошибка которую обязательно нужно исправить. При открытом множестве — не будет даже предупреждения. Разве что линтер в очередной раз незамеченный вздохнёт про "место для битья головой", но это не будет ничем отличаться от всех предыдущих раз.

0

Как раз наоборот, для обратной совместимости есть специальный способ заставить писать дефолт матчер, даже если все ветки обрабатываются, на тот случай, если в будущем появится новый вариант. См линк

0

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

-12
Запускали с lto=true?

Это не лто, а «лто» — оно не особо поможет. А даже если бы это было настоящие лто, то оно бы расту так же не помогло, ведь он состоит из косвенных вызовов на 99%, а значит llvm без девиртуализации никакогда не узнает — какую именно функцию код может вызвать.

В коде не должно быть unwrap

unwrap — это основа раст-пропаганды. Без unwrap через unwrap раст-портянки будут попросту километровым нечто. Аналогично с expect, когда его везде пихают в надежде, что неофиты/сторонние наблюдатели подумают, что там есть исключения.
+4
Это не лто, а «лто» — оно не особо поможет. А даже если бы это было настоящие лто, то оно бы расту так же не помогло, ведь он состоит из косвенных вызовов на 99%, а значит llvm без девиртуализации никакогда не узнает — какую именно функцию код может вызвать.

Эмм, это вообще какой-то поток сознания.


Вы сейчас линкуете все подряд, и жалуетесь на большой бинарь.


Для сравнения, когда я писал бота с http сервисом, релиз сборка весила 130 мегабайт, с lto — 6. Как по мне, разница существенная.


unwrap — это основа раст-пропаганды. Без unwrap через unwrap раст-портянки будут попросту километровым нечто. Аналогично с expect, когда его везде пихают в надежде, что неофиты/сторонние наблюдатели подумают, что там есть исключения.

Да ну? Открываем https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap


In general, because this function may panic, its use is discouraged. Instead, prefer to use pattern matching and handle the None case explicitly.

Где же раст пропаганда?

+4

Только что проверил,
без lto: 2,61 MB (2 737 543 bytes)
с lto: 1,04 MB (1 101 258 bytes)


Собирал stable-x86_64-pc-windows-gnu - rustc 1.34.1 (fc50f328b 2019-04-24)

+2
Может в ваших словах и есть какой-то смысл, но за вашим языком его не то что уловить трудно — его даже искать не хочется, настолько убого вы выражаете в данный момент мысли.
-18
Эмм, это вообще какой-то поток сознания.

Т.е. адепт раста не осилил понять, вернее осилил и слился, а проблема моя? Почему адепт не знает что такое thinlto и какие у него есть ограничения, но пытается со мною спорить? Мануал есть, написан.

Вы сейчас линкуете все подряд, и жалуетесь на большой бинарь.

Лично я ничего не линкую.

Да ну? Открываем doc.rust-lang.org/std/option/enum.Option.html#method.unwrap

Ну, открываем и? Открываем любой раст-портянку из любой раст-агитки и видим там unwrap через unwrap через expect.

Где же раст пропаганда?

К чему вы это процитировали? Очевидно, что у любой пропаганды созданы пути отхода. В этом суть пропаганды, что она неявная. Эта портянка не являются частью любой раст-портянке, которая используется в пропаганде. В этом смысл.

Что человек, который привык к исключениям и прочему видя это думает, что «так пишут код», но так код не пишут. И именно этим занимается пропаганда, что она выдаёт неюзабельные портянки за «полноценные».

А если мы начинаем заменять всё это на матчи, то портянка из 100строк превращается в 250. На это и делается расчёт.

+3

Вообще отвечать хаму такое себе, но завершая начатое


Это не лто, а «лто» — оно не особо поможет. А даже если бы это было настоящие лто, то оно бы расту так же не помогло, ведь он состоит из косвенных вызовов на 99%, а значит llvm без девиртуализации никакогда не узнает — какую именно функцию код может вызвать.

На всю стандартную библиотеку динамическая диспетчеризация используется ровно в 38 файлах:


https://www.google.com/search?q=site%3Agithub.com%2Frust-lang%2Frust%2Fblob%2Fmaster%2Fsrc%2Flibstd+dyn&oq=site%3Agithub.com%2Frust-lang%2Frust%2Fblob%2Fmaster%2Fsrc%2Flibstd+dyn&aqs=chrome..69i64.2936j0j7&sourceid=chrome&ie=UTF-8

+2
lto=true
Бинарь 940 Кб, то есть экономия более чем в 2 раза
Почему-то прохождение теста немного ускорилось — 3.95 сек, возможно экономия на чтении бинаря.
+6

Компактный бинарь всегда более кэш-френдли, это имеет небольшое влияние на производительность.




Кстати, для замеров раста рекомендую использовать criterion.

0

Нет, не всегда. У вас же нет цели весь бинарь в кеш загнать.


Гы, в раст портировали хаскелевский критерион, прикольно.

0
Подсчет ссылок часто медленнее — ссылки надо инкрементить/декрементить при каждом присваивании и выходе из области видимости, хуже того, это надо делать атомарными операциями.
Преимущество в том, что накладные расходы размазаны по времени работы, а у gc они появляются в самый неожиданный момент времени (вообще говоря у счетчика ссылок тоже может возникнуть непредсказуемая задержка, если освобождается сразу длинная цепочка объектов, но все-таки это меньше, чем в GC).
0
У современных параллельных сборщиков расходы тоже размазаны по времени и практически все этапы идут параллельно выполнению кода. Нужные только где-то две паузы на считанные миллисекунды, а то и сотни микросекунд.
Преимущество подсчета ссылок только в том, что расход памяти под контролем и предскауемые задержки. Тем не менее эти задержки могут быть очень большие из-за упомянутых цепочек объектов. Да и циклические ссылки не имеет разруливать.
+3
Протестируйте на своём железе версию на Perl — у меня менее 10 секунд, хотя язык скриптовый, сборщик мусора тоже подсчетом ссылок (как и Swift), и код значительно короче обеих в статье.
#!/usr/bin/perl

use v5.12;
use warnings;

use JSON::XS;
use List::Util qw(first);
use Time::HiRes 'time';

my $jsontext;

my $t0 = time;

die "Specify input file as first argument" unless defined $ARGV[0] && -f $ARGV[0];
open FH, "<", $ARGV[0] or die "Can't open input file $ARGV[0]: $!";
# slurp all file at once :)
{
    local $/ = undef;
    $jsontext = <FH>;
    close FH;
}
my $t1 = time;

say "read file in " . ($t1-$t0) . " sec";

my $data = decode_json $jsontext;

my $t2 = time;
say "decoded JSON in " . ($t2-$t1) . " sec";

my (@debtors, %by_phone);

for my $rec (@$data) {
    my ($name, @phones, $di);

    if (ref $rec->{company}) {  # XXX only HASH
        $name = $rec->{company}->{name};
    } else {
        $name = $rec->{company};
    }

    push @phones, $rec->{phone} if exists $rec->{phone};
    if (ref $rec->{phones}) {
        push @phones, @{ $rec->{phones} };
    } else {
        push @phones, $rec->{phones};
    }

    my $ph = first { exists $by_phone{$_} } @phones;
    if (not defined $ph) {
        push @debtors, {
            companies   => [ $name ],
            debt        => 0,
            phones      => { map { $_ => 1 } @phones },
        };
        $di = $#debtors;
        $by_phone{$_} = $di foreach @phones;
    } else {
        $di = $by_phone{$ph};
        push @{ $debtors[$di]->{companies} }, $name
            unless scalar grep { $_ eq $name } @{ $debtors[$di]->{companies} };
        $debtors[$di]->{phones}->{$ph} = 1;
        foreach (@phones) {
            die "Oops! consistency error... $_ $di"
                if defined $by_phone{$_} and $by_phone{$_} != $di; 
            $by_phone{$_} = $di;
            $debtors[$di]->{phones}->{$_} = 1;
        }
    } 
    $debtors[$di]->{debt} += $rec->{debt};
}

my $t3 = time;
say "aggregated in " . ($t3-$t2) . " sec";

$" = ", ";
for my $debtor (@debtors) {
    say "\nCompany names:\t@{ $debtor->{companies} }";
    say "Total debt:\t$debtor->{debt}";
    say "Phone numbers:\t@{[sort keys %{ $debtor->{phones} }]}";
}

say "\nTotal " . (time-$t0) . " sec";
0
read file in 0.0822980403900146 sec
decoded JSON in 1.64080214500427 sec
aggregated in 7.59123682975769 sec
Wide character in say at ./fastpivot.pl line 78.

Total 9.31448316574097 sec
==================
Это похоже на правду, подобные цифры демонстрирует у меня Javascript. Что ставит вопрос — а так ли уж нужны компилируемые языки для прикладного программирования (го ?). А что касается перла — да, он очень крут.
+1
Ну, на самом деле, там модуль JSON::XS написан на Си — впрочем, Javascript поступает аналогично. Если заменить на JSON::PP, на моем железе парсинг был 143 секунды и около полгигабайта памяти. Но агрегация всё равно остается около 7 секунд, т.е. шаги 3+4 в статье — непонятно, почему Swift, будучи более типизированным, не ускоряется от этого.
0
Тут приводили скрин профайлера — видно что именно кастинг типов и тормозит, изначально то мы имеем Any, который нужно приводить к строкам и числам, а дайнамик кастинг — это видно сильная сторона именно перла.
+1
Там все сложнее. Многие вызовы свифта уходят в недра Foundation и CoreFoundation библиотек, а это ObjC и С. Там куча рантайм вызовов, жрущих буквально секунды времени на бриджинг между разными языками.
0
Я запускал под макосью, как и товарищ со скрином выше, чтобы профайл посмотреть. Что там будет под линуксом это вопрос хороший. Тот же JSON парсер под максью уходит в NSJSONSerialization.
+2
Что ставит вопрос — а так ли уж нужны компилируемые языки для прикладного программирования (го ?)

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

+1
Ну вот, Perl и Rust реальные конкуренты на хайлоад, остальные языки слились :)
0
По жаваскрипту я делал замеры, у меня в блоге 3 статьи на эту тему, короче, миллион объектов он за 9 секунд агрегирует, то есть где-то в 2.5 раза медленнее раста получается. На гошке я почти не писал, может изнасилую себя и сделаю тест, не знаю.
0
Непонятно как в хромиуме это померять, понятно что на порядок.
0

Я думал оно нодой обрабатывалось. А так можно посмотреть в диспечере задач сколько вкладка/devtool памяти кушает. Или через профайлинг пытаться выяснять.

+4

Я немного подчистил Rust реализацию в плане идиоматических конструкций (как я это вижу): https://gist.github.com/frol/547f36c5f736f3651dfeac53d8fde34e (174 строки кода, если это важно)


Мне всё ещё не нравится:


  • именование переменных
  • обработка ошибок
  • глубина лесенки в некоторых местах (но я не стал дробить код)

P.S. obj.clear() вместо obj = Vec::new() дало ~13% ускорение за счёт переиспользования буфера вместо деалокации старого и алокации нового буфера.

0
Спасибо, очень приятный код. Исполнение 3.5 секунды. Про clear() не знал.
PS
Что касается map — ну не люблю я этого, в JS объелся. Цикл же нагляднее, понятно в каком месте преждевременно выходим, не нужно помнить спецификации map() на всех языках. Единственное ЗА — это возможность оптимизации (в т.ч. распараллеливания), но похоже Rust этого не умеет?
+2

Умеет, с помощью библиотеки Rayon, например. Насчет итераторы vs циклы я с вами не соглашусь.
Во-первых, во всех языках map действует одинаково и поддерживает вполне определенные инварианты. Чтобы понять, что получается в цикле — надо его полностью распарсить глазами. Чтобы понять, что на выходе итератора — достаточно прочитать только названия методов и убедиться в том, что map, например, трансформирует каждый элемент из одного типа в другой, но никогда не добавляет новых элементов и не удаляет старых.


Кроме того, Rust отлично оптимизирует итераторы, убирая проверки на выход за границы коллекций.

0
Ну не знаю. Если читать буквально Ваш код — мы сначала отфильтровали массив (то есть пробежались по всем телефонам), а потом взяли первый элемент результата. Я понимаю конечно, что мы выходим из цикла досрочно, и это «досрочно» стоит в самом конце… в общем, пока не могу привыкнуть.
PS
Впрочем, согласен, par_iter() это то что надо!
+2
Нее у нас ленивое вычисление. Мы никуда не бежим вначале.

P.S. Ааа я понял вы имелли виду что это не сразу понятно из кода. Тогда да надо привыкнуть, что мы небудем ничего делать пока явно это не попросим.
+3
Единственное ЗА — это возможность оптимизации (в т.ч. распараллеливания), но похоже Rust этого не умеет?

rayon умеет. То есть простая замена .iter() на .par_iter() и магия работает. Но в данном случае нужно найти первое вхождение, так что смысла в этом мало. Если бы это было узким местом, то стоило бы заменить вектор (с его линейным поиском) на hashmap.

+1
Может и напишу. Хотя, гошник бы потратил в 10 раз меньше времени чем я, вспоминая как чего там называется. Да и не люблю я Go, эстетически.
+3
1) Буферизованное посимвольное чтение файла, потоковый парсинг и выделение объектов из массива.

Если в случае Rust это видимо так, то в случае Swift никакого буфера для чтения не используется. Мало того, на каждый вызов чтения вы создаете новый Data объект.
Если это заменить на InputStream, то становится 13 секунд (https://gist.github.com/ivan-ushakov/65ad074216e33f222b6a572069e4e8f5). Это по прежнему много, но тут проблема в том что текущий парсер JSON не сохраняет информацию о типе при разборе, и вынуждает использовать as? для логики, что выливается в _dynamicCastFromExistential, который занимает много времени, судя по отчету Time Profiler.
Еще не очень корректно сравнивать язык с постоянным ARC, и язык где ARC можно использовать только когда он нужен. В вашем коде Swift почти не предпринято усилий по минимизации создания объектов в куче.
0
О! Очень дельный комментарий. Я так и предполагал, что в Swift нужно хорошо разбираться, чтобы писать оптимально, иначе выстрел в ногу. То, что я Data повторно создаю — на эти же грабли я наступил в Rust, и мне уже объяснили, что надо не пересоздавать объект, а просто его очищать, тогда релокации не происходит.
Что касается типа Any — это кстати очень хороший довод в пользу алгебраических типов Rust, где нет корневого типа (в отличие например от Go, где он есть interface{}, и наверняка так же долго кастится). А в растовом enum получается, информация о типе сохраняется, потому что там кастинг проходит быстро.
PS
13 и 4.5 секунды — это уже нормальная, объяснимая разница.
PPSS
Специалисты смогли ускорить мой код на Rust всего до 3.5, видимо потому что вариантов написать по другому значительно меньше, компилятор злой :)
0
Ну тогда надо использовать. Вот как в расте выглядит значение JSON (и никаких Any):
enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec),
Object(Map<String, Value>),
}
0
В таком случае в Swift тоже нужно искать библиотеку для парсинга JSON не использующую встроенный в Foundation парсер. А для Swift таких библиотек и нет, нашлась разве что discontinued github.com/tyrone-sudeium/JSONCore
0
Да, Вы волшебник, я подозревал что-то в этом роде.
PS
if d.phones[p] != nil {
d.phones[p] = 1
}
Я стараюсь таких вещей избегать, если компилятор тупой, он по идее должен дважды хеш брать от p, но, выходит, не берет.
+1
Перепроверил — с кастомным типом переменной p хэш при таком коде вычислится дважды. Хотя возможно, что в Swift хеши для строк кешируются и в этом месте не будет провалов по производительности.
+1

Ради интереса выпилил весь ужасающий парсинг, заменив на дефолтные растовые возможности. Стриминг при этом не потерялся. Попробуйте это забенчить, интересно, что у вас выйдет:


https://gist.github.com/Pzixel/4427eb6641cbd092621ea91c5fd0db82




P.S. Также попробовал полностью с нуля переписать, адекватно, с парсингом, но забил после того, как увидел, что у вас в некоторых случаях phones приходят из двух разных полей, причем они могут оба в одном объекте быть. К такому жизнь serde не готовила :) Но если интересно, то практически все варианты нормально парсятся вот здесь: https://gist.github.com/Pzixel/0460abea262574b6081dc709c9c30e26. Там можно увидеть, что почти все разбросанные данные можно распарсить в структуру, ну а дальше что с ней делать вопрос тривиальный.

0
5.6 секунд почему-то.
Самый быстрый вариант получился у фрола:
habr.com/ru/post/450512/#comment_20119542
PS
Я идею Вашу понял, но тут, похоже мы теряем контроль процесса, отдавая все библиотеке. У меня была долгосрочная идея — прицел на гигабайтные файлы и миллиард объектов. Соответственно, сейчас пишу сюда многопоточность, так как чтение и первичный парсинг быстрее раз в 5 чем остальное, значит можно 5 тредов запустить и раздавать объекты им в очередь. Напишу — выложу. Дальше ускоряться можно только двумя путями:
— SIMD, которого у меня нет
— выкинуть serde, и писать свой однопроходный потоковый парсинг, чтобы одним циклом по байт-буферу делать все — вплоть до агрегации, и без промежуточных структур.
PS
Спасибо большое, Вы с фролом мне очень помогли подтянуть язык, перестал брезговать функциональщиной :)
+1
Самый быстрый вариант получился у фрола:

Я его и взял за основу. По сути я просто заменил process_file на стандартную функцию, остальное оставил без изменений.


Замедление в целом понятно откуда взялось, потому что теперь JSOn парсится правильно. например, что ваш парсер скажет на объект {"company":"Рога и } копыта", "debt": 800, "phones": [123, 234, 456]},? Правильно, скорее всего помрет. А трекинг когда мы внутри или снаружи объекта и даст тут скорее всего немного лишних миллисекунд.


Если переписать абсолютно все декларативно то мб получится выжать побольше. А может и нет, надо мерить.


У меня была долгосрочная идея — прицел на гигабайтные файлы и миллиард объектов.

Никак этому не мешаю.


Соответственно, сейчас пишу сюда многопоточность, так как чтение и первичный парсинг быстрее раз в 5 чем остальное, значит можно 5 тредов запустить и раздавать объекты им в очередь. Напишу — выложу. Дальше ускоряться можно только двумя путями:

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


Спасибо большое, Вы с фролом мне очень помогли подтянуть язык, перестал брезговать функциональщиной :)

Всегда пожалуйста. Декларативное описание всегда лучше императивного. Хотя бы потому, что за счет ограничений накладываемых логикой операции компилятор может дополнительно оптимизировать, самое банальное — выкинуть баунд чеки на map. Ну и то что читать это проще вам выше уже говорили. Я про это рассказывал здесь.

0
Да, полностью согласен, мой парсер и не парсер вовсе :)
Надо будет изучить нормальные потоковые парсеры, вроде на си и жаве их полно, на расте нет вообще, когда-то давно при работе с XML они просто и рядом не лежали по производительности с тормознутыми DOM. Проблема, что мало людей захотят пользоваться, и путаться в аде колбэков…
+1

Ну вот serde потоково парсит все. Просто по-дефолту он парсит в какую-то структуру данных, но можно сделать визитор и в visit_seq просматривать элементы и делать что угодно. Кстати, возможность так делать я утащил отсюда.

0
Замедление в целом понятно откуда взялось, потому что теперь JSOn парсится правильно. например, что ваш парсер скажет на объект {«company»:«Рога и } копыта», «debt»: 800, «phones»: [123, 234, 456]},? Правильно, скорее всего помрет. А трекинг когда мы внутри или снаружи объекта и даст тут скорее всего немного лишних миллисекунд.

Я таки заморочился, обрабатываю кавычки и экранированные кавычки (вроде ничего не забыл ?). Производительность упала, пришлось отказаться от второго буфера байтов (кроме случая, когда объект разорван первым буфером). Получилось немного медленней чем у frol (3.95) но все равно быстрее чем на serde.

Подозреваю, что не serde тормозит, а чтение из стрима — from_reader(), оно побайтовое с необходимостью каждый байт анрапить. Похожую проблему я обнаружил тут:
habr.com/ru/post/450512/#comment_20120520
PS
Полный код:
main.rs
//[dependencies]
//serde_json = "1.0"

use std::collections::{HashMap, HashSet};
use serde_json::Value;

const FILE_BUFFER_SIZE: usize = 100000;

//source data
#[derive(Default)]
struct DebtRec {
    company: String,
    phones: Vec<String>,
    debt: f64
}

//result data
#[derive(Default)]
struct Debtor {
    companies: HashSet<String>,
    phones: HashSet<String>,
    debt: f64
}

#[derive(Default)]
struct Debtors {
    all: Vec<Debtor>,
    index_by_phone: HashMap<String, usize>
}


fn main() {
    let mut result = Debtors::default();

    let mut fflag = 0;
    let mut threads = vec![];
    let mut tid = 0;

    for arg in std::env::args() {
        if arg == "-f" {
            fflag = 1;
        }
        else if fflag == 1 {
            fflag = 2;
            tid += 1;
            threads.push(std::thread::spawn(move || process_file(tid, &arg)));
        }
    }

    for t in threads {
        let part = t.join().unwrap();
        if result.all.len() == 0 {
            result = part;
        } else {
            merge_result(part, &mut result);
        }
    }

    for (di, d) in result.all.iter().enumerate() {
        println!("-------------------------------");
        println!("#{}: debt: {}", di+1, &d.debt);
        println!("companies: {:?}\nphones: {:?}", &d.companies, &d.phones);
    }

    if fflag < 2 {
        println!("USAGE: fastpivot -f \"file 1\" -f \"file 2\" ...");
    }
} 


fn process_file(tid: i32, fname: &str) -> Debtors { 
    use std::io::prelude::*;

    let mut result = Debtors::default();

    println!("thread {}: file {}", tid, fname);
    let tbegin = std::time::SystemTime::now();
    let mut prntab = String::new();
    for _ in 0..(tid-1)*3 {prntab.push('\t')}

    let mut allcount = 0;
    let mut prncount = 0;
    let mut errcount = 0;

    let mut file = match std::fs::File::open(fname) {
        Ok(f) => f,
        Err(e) => {
            println!("ERROR: {}", e);
            return result;
        }
    };

    let mut buf = [0; FILE_BUFFER_SIZE];
    let mut osave:Vec<u8> = vec![];
    let mut i0 = 0;
    let mut braces = 0;
    let mut quotes = false;
    let mut backslash = false;

    loop {
        let blen = match file.read(&mut buf) {
            //Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Ok(0) => break,
            Ok(blen) => blen,
            Err(e) => {
                println!("ERROR: {}", e);
                return result;
            }
        };
        for i in 0..blen {
            let b = buf[i];

            if b == b'"' && !backslash {
                quotes = !quotes;
            }
            backslash = b == b'\\';

            if !quotes {
                if b == b'{' {
                    if braces == 0 {
                        i0 = i;
                    }
                    braces += 1;
                }
                else if b == b'}' {
                    braces -= 1;
                    if braces == 0 { //object formed !
                        let mut o = &buf[i0..i+1];
                        i0 = 0;
                        if osave.len() > 0 {
                            osave.extend_from_slice(o);
                            o = &osave;
                        }
                        match serde_json::from_slice(o) {
                            Ok(o) => {
                                process_object(&o, &mut result);
                            }
                            Err(e) => {
                                println!("JSON ERROR: {}:\n{:?}", e, std::str::from_utf8(o));
                                errcount +=1;
                            }
                        }
                        if osave.len() > 0 {
                            osave.clear();
                        }
                        allcount += 1;
                        prncount += 1;
                        if prncount == 100000 {
                            println!("{}thread {}: {}", prntab, tid, allcount);
                            prncount = 0;
                        }
                    }
                }
            } 
        }
        if i0 > 0 {
            osave.extend_from_slice(&buf[i0..]);
            i0 = 0;
        }
    }

    println!("thread {}: file {}: processed {} objects in {:?}s, {} errors", 
        tid, fname, allcount, tbegin.elapsed().unwrap(), errcount
    );

    result
}


fn process_object(o: &Value, result: &mut Debtors) {
    let dr = extract_data(o);
    //println!("{} - {:?} - {}", &dr.company, &dr.phones, &dr.debt,);

    let di = match dr.phones.iter().filter_map(|p| result.index_by_phone.get(p)).next() {
        Some(i) => *i,
        None => {
            result.all.push(Debtor::default());
            result.all.len()-1
        }
    };
    let d = &mut result.all[di];
    d.companies.insert(dr.company);
    for p in &dr.phones {
        d.phones.insert(p.to_owned()); 
        result.index_by_phone.insert(p.to_owned(), di);
    }
    d.debt += dr.debt;
}


fn merge_result(part: Debtors, result: &mut Debtors) {
    for dr in part.all {
        let di = match dr.phones.iter().filter_map(|p| result.index_by_phone.get(p)).next() {
            Some(i) => *i,
            None => {
                result.all.push(Debtor::default());
                result.all.len()-1
            }
        };
        let d = &mut result.all[di];
        d.companies.union(&dr.companies);
        for p in &dr.phones {
            d.phones.insert(p.to_owned());
            result.index_by_phone.insert(p.to_owned(), di);
        }
        d.debt += dr.debt;
    }
}


fn extract_data(o: &Value) -> DebtRec {
    let mut dr = DebtRec::default();

    let c = &o["company"];
    dr.company = match c {
        Value::Object(c1) => match &c1["name"] {
            Value::String(c2) => c2.to_owned(),
            _ => val2str(c)
        },
        _ => val2str(c)
    };

    match &o["phones"] {
        Value::Null => {}
        Value::Array(pp) => dr.phones.extend(pp.iter().map(|p| val2str(p))),
        pp => dr.phones.push(val2str(&pp))
    }

    match &o["phone"] {
        Value::Null => {}
        p => dr.phones.push(val2str(&p))
    }

    dr.debt = match &o["debt"] {
        Value::Number(d) => d.as_f64().unwrap_or(0.0),
        Value::String(d) => d.parse::<f64>().unwrap_or(0.0),
        _ => 0.0
    };

    dr
}


fn val2str(v: &Value) -> String {
    match v {
        Value::String(vs) => vs.to_owned(), //to avoid additional quotes
        _ => v.to_string()
    }
}
0
Да, конечно я понимаю, что когда схема строгая, весь этот бред с матчингом не нужен — сразу парсим в строгую структуру, но мне хотелось понять как работает динамическая. Получается, что кастинг типов — это был основной тормоз свифта, а в расте все норм, енамы отностительно быстро матчатся, но правда не мгновенно.
0

Ну тут можно дополнительно ускорять, убирая все String, где можно преобразуя их в u64 (например, номера телефонов, если позволяет бизнес). В общем, варианты есть. Но вряд ли они прям супер-много дадут, может еще процентов 10-20.

0
Хочется раз в 100, а это только CUDA (там вроде из шейдера можно писать в разделяемую память, в отличие от шейдеров обычных), но не на каждом сервере/компе эта железка будет. На новых интелах вроде SIMD добавили в центральный процессор, но как пользоваться я пока не разобрался.
+2

Да тут уже вопросы к диску больше будут, чем к производительности. Больше 400МБ/сек выжать не получится никак.


С другой стороны не очень понятно, зачем такие объемы данных парсить в кривом формате.

-1

Даже на каком-нибудь топовом NVMe, который поддерживает 2-3ГБ/с скорость (последовательное чтение, понятное дело)?

0
Спасибо огромное!!!
6.73 секунды, бинарь 4.8 Мб, компилил go build main.go
В гошке я особо не сомневался, думаю, если аккуратно допилить код — сможет догнать растишку :)
NishchebrodKolya2
vitvakatu
0
Как разнятся результаты. Видимо, надо учитывать конфигурацию железа )))
0
Не, ну вот у меня сейчас все варианты есть, даже пришлось перл поставить с либами. Пока раст в исполнении фрола выдает 3.6 секунды, и это рекорд. Кстати, измерил память гошки:
Go — VSZ:10.7 Мб, RSS: 7.7 Мб
То есть по расходу памяти у свифта нет шанса, раст чуть-чуть экономнее, напомню:
Swift — VSZ: 205 Мб, RSS: 21 Мб
Rust — VSZ: 13 Мб, RSS: 1Мб
PS
Поставьте себе раст и поиграйтесь — если сможете его почти победить — ваша статья будет сразу в топе :)
PPSS
Память не течет, тестил на гигабайтном файле, так что GC работает норм :)
0
У меня линукс 64. Но в память это не честно, в моей задаче надо гигабайты парсить.
+1
Залил новый код: gist.github.com/tsukanov-as/d1532303e16d757e3fa9e70ccb21d64b
Пардон за его страшный вид )
У меня на i5 8400 3.6 сек. и 4 мб. памяти
Память смотрел на глаз по диспетчеру, т.к. не знаю как ее мерить на винде.
Для сравнения вариант на Go у меня 2.5 сек. и 6 мб. памяти.

ps Скорее всего кривое как мои руки, но на тестовом файле вроде правильно отработало
0
Получается, что гошка вторым номером идет против раста, остальные медленнее. Так всегда — гугл изобретает странные корявые продукты, но они сцуко работают быстрее, чем красивые и продуманные чужие. Хромиум хуже мозилы, но быстрее, гугловский дарт хуже тайпскрипта, но в мобилы будут пихать именно его, раст красивее гошки, но вакансий нет и т.д.
0
Есть еще LuaJit, который с учетом ffi и грязных трюков (типа mmap) думаю обойдет Go. Но, как это ни печально, он похоже умирает.
0
Программирование это не музыка, где побеждает новое звучание. Это грязная корпоративная война, где побеждает влияние. Сказала одна компания что публичные методы с большой буквы надо писать, все сказали есть. А все потому, что музыкант он условно в одиночку может сделать продукт, а программистов нужно сотни, чтобы что-то реальное написать. Поэтому остается лишь встраиваться в чужие тренды.
0
Таки запустил этот код на LuaJit из любопытства.
1.5 секунды и 2 Мб памяти

ps Допускаю что ошибаюсь
0
А в чем трюк? Я понимаю, что избавился от промежуточной структуры, значит минус аллокация памяти. Видимо файл читается быстрее, но тут надо тестировать на одной платформе — допускаю что Линукс тормознутее винды при работе с файлами.
Я сейчас на расте достиг 2.5 секунд за счет многопоточности, если выкину промежуточную структуру, возможно с трудом в полторы втиснусь, а как Вам удается такие цифры показывать?
0
Ну в коде трюк только один:
s:find("^%b{}", p)
Это хитрая луашная «регулярка», которая умеет рекурсивно искать сбалансированные скобки.
А в целом читаю файл блоками, пытаюсь этой чудо регуляркой найти сбалансированные скобки, разбираю парсером то, что нашел, оставшийся кусок блока приклеиваю к следующему и по кругу.
0
Ровно то же самое делаю и я. Только не забудьте, что скобка может встречаться внутри кавычек «рога и {копыта», а еще бывает экранированная кавычка, это ж json. Вот мой последний код:
main.rs
//[dependencies]
//serde_json = "1.0"

use std::collections::{HashMap, HashSet};
use serde_json::Value;

const FILE_BUFFER_SIZE: usize = 100000;
const PRN_COUNT: i32 = 100000;

//source data
#[derive(Default)]
struct DebtRec {
    company: String,
    phones: Vec<String>,
    debt: f64
}

//result data
#[derive(Default)]
struct Debtor {
    companies: HashSet<String>,
    phones: HashSet<String>,
    debt: f64
}

#[derive(Default)]
struct Debtors {
    all: Vec<Debtor>,
    index_by_phone: HashMap<String, usize>
}


fn main() {
    let mut result = Debtors::default();

    let mut threadcount = -1;
    let mut fflag = 0;
    for arg in std::env::args() {
        if arg == "-t" {
            threadcount = 0;
        }
        else if threadcount == 0 {
            threadcount = match arg.parse() {
                Ok(n) => n,
                Err(_) => {
                    println!("ERROR: -t \"{}\" - must be an integer !", arg);
                    break;
                }
            }
        }
        else if threadcount > 0 {
            if arg == "-f" {
                fflag = 1;
            }
            else if fflag == 1 {
                fflag = 2;
                let resultpart = process_file(&arg, threadcount as usize);
                if result.all.len() == 0 {
                    result = resultpart;
                } else {
                    merge_result(resultpart, &mut result);
                }
            }
        }
    }

    for (di, d) in result.all.iter().enumerate() {
        println!("---------------------------------------------");
        println!("#{}: debt: {}", di+1, &d.debt);
        println!("companies: {:?}\nphones: {:?}", &d.companies, &d.phones);
    }

    if threadcount <= 0 || fflag < 2 {
        println!("USAGE: fastpivot -t <thread count> -f \"file name\" -f \"file name\" ...");
    }
} 


fn process_file(fname: &str, threadcount: usize) -> Debtors { 
    use std::io::prelude::*;

    println!("file {}:", fname);
    let tbegin = std::time::SystemTime::now();

    let mut file = match std::fs::File::open(fname) {
        Ok(f) => f,
        Err(e) => {
            panic!(e);
        }
    };

    let mut buf = [0; FILE_BUFFER_SIZE];
    let mut i0 = 0;
    let mut osave:Vec<u8> = vec![];

    let mut braces = 0;
    let mut quotes = false;
    let mut backslash = false;

    let mut channels = vec![];
    let mut threads = vec![];
    for tid in 0..threadcount { //start threads
        let (send, recv) = std::sync::mpsc::channel();
        channels.push(send);
        threads.push(std::thread::spawn(move || process_thread(recv, tid)));
    }
    let mut tid = 0;

    loop {
        let blen = match file.read(&mut buf) {
            //Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Ok(0) => break,
            Ok(blen) => blen,
            Err(e) => {
                panic!(e);
            }
        };
        for i in 0..blen {
            let b = buf[i];

            if b == b'"' && !backslash {
                quotes = !quotes;
            }
            backslash = b == b'\\';

            if !quotes {
                if b == b'{' {
                    if braces == 0 {
                        i0 = i;
                    }
                    braces += 1;
                }
                else if b == b'}' {
                    braces -= 1;
                    if braces == 0 { //object formed !

                        let mut o = &buf[i0..i+1];
                        i0 = 0;
                        if osave.len() > 0 {
                            osave.extend_from_slice(o);
                            o = &osave;
                        }

                        channels[tid].send(o.to_vec()).unwrap();
                        tid = if tid == threadcount-1 {0} else {tid+1};

                        osave.clear();
                    }
                }
            } 
        }
        if i0 > 0 {
            osave.extend_from_slice(&buf[i0..]);
            i0 = 0;
        }
    }

    for tid in 0..threadcount { //stop threads
        channels[tid].send(vec![]).unwrap();
    }
    println!("---------------------------------------------");

    let mut result = Debtors::default();
    let mut allcount0 = 0;
    let mut errcount0 = 0;

    for _ in 0..threadcount {
        let (resultpart, allcount, errcount) = threads.pop().unwrap().join().unwrap();
        if result.all.len() == 0 {
            result = resultpart;
        } else {
            merge_result(resultpart, &mut result);
        }
        allcount0 += allcount;
        errcount0 += errcount;
    }

    println!("---------------------------------------------");
    println!("file {}: processed {} objects in {:?}s, {} errors", 
        fname, allcount0, tbegin.elapsed().unwrap(), errcount0
    );

    result
}


fn process_thread(channel: std::sync::mpsc::Receiver<Vec<u8>>, tid: usize) -> (Debtors, i32, i32) {
    let mut result = Debtors::default();
    let mut allcount = 0;
    let mut errcount = 0;
    let mut prncount = 0;
    let mut prntab = String::new();
    for _ in 0..(tid)*3 {prntab.push('\t')}

    loop {
        let o = channel.recv().unwrap();
        if o.len() == 0 {
            break;
        }
        match serde_json::from_slice(&o) {
            Ok(o) => {
                process_object(&o, &mut result);
            }
            Err(e) => {
                println!("JSON ERROR: {}:\n{:?}", e, std::str::from_utf8(&o));
                errcount +=1;
            }
        }
        prncount += 1;
        if prncount == PRN_COUNT {
            allcount += prncount;
            prncount = 0;
            println!("{}thread{}: {}", prntab, tid, allcount);
        }
    }
    allcount += prncount;
    (result, allcount, errcount)
}


fn process_object(o: &Value, result: &mut Debtors) {
    let dr = extract_data(o);
    //println!("{} - {:?} - {}", &dr.company, &dr.phones, &dr.debt,);

    let di = match dr.phones.iter().filter_map(|p| result.index_by_phone.get(p)).next() {
        Some(i) => *i,
        None => {
            result.all.push(Debtor::default());
            result.all.len()-1
        }
    };
    let d = &mut result.all[di];
    d.companies.insert(dr.company);
    for p in &dr.phones {
        d.phones.insert(p.to_owned()); 
        result.index_by_phone.insert(p.to_owned(), di);
    }
    d.debt += dr.debt;
}


fn merge_result(part: Debtors, result: &mut Debtors) {
    for dr in part.all {
        let di = match dr.phones.iter().filter_map(|p| result.index_by_phone.get(p)).next() {
            Some(i) => *i,
            None => {
                result.all.push(Debtor::default());
                result.all.len()-1
            }
        };
        let d = &mut result.all[di];
        d.companies.union(&dr.companies);
        for p in &dr.phones {
            d.phones.insert(p.to_owned());
            result.index_by_phone.insert(p.to_owned(), di);
        }
        d.debt += dr.debt;
    }
}


fn extract_data(o: &Value) -> DebtRec {

    fn val2str(v: &Value) -> String {
        match v {
            Value::String(vs) => vs.to_owned(), //to avoid additional quotes
            _ => v.to_string()
        }
    }

    let mut dr = DebtRec::default();

    let c = &o["company"];
    dr.company = match c {
        Value::Object(c1) => match &c1["name"] {
            Value::String(c2) => c2.to_owned(),
            _ => val2str(c)
        },
        _ => val2str(c)
    };

    match &o["phones"] {
        Value::Null => {}
        Value::Array(pp) => dr.phones.extend(pp.iter().map(|p| val2str(p))),
        pp => dr.phones.push(val2str(&pp))
    }

    match &o["phone"] {
        Value::Null => {}
        p => dr.phones.push(val2str(&p))
    }

    dr.debt = match &o["debt"] {
        Value::Number(d) => d.as_f64().unwrap_or(0.0),
        Value::String(d) => d.parse::<f64>().unwrap_or(0.0),
        _ => 0.0
    };

    dr
}
0
Да, решение на Lua грязное в этом плане.
Для честного разбора нужно побайтно перебирать, а это заведомо провально на интерпретаторе и даже пытаться смысла нет.
Просто перебрать побайтно файл в памяти у меня 45 секунд на Lua 5.3.5
Изначально я вообще хотел попробовать использовать SAX парсер, но не нашел такой либы.
0
Для раста есть подобие SAX-парсера, только оно медленнее моего алгоритма. Но если в луа регулярки сишные, то можно наверное на регулярке что-то написать. Я просто давно понял, что если нужна экстремальная производительность — лучше самому в едином цикле все сделать, чем либами обкладываться. Поэтому и раст. Тема с гошкой интересная, закончу свой многопоточный вариант, попробую доработать Ваш код, чтобы с горутинами, неужели не догонит.
+1
Не знаю откуда я взял эти 45 секунд. На самом деле 8 секунд. Взял обидел Lua ненароком
0
Поставил у себя Rust и собрал этот код.
2 сек. и около 100 Мб памяти (не знаю что это значит) в 1 поток.

Процессор у меня i5 8400 6 ядер
В 6 потоков 0.5 сек и непонятно сколько памяти (но как будто бы мало)
Каждый поток стабильно добавляет скорости.
0
По потокам наверное статью придется писать. Во-первых много памяти жрут, во-вторых если буфер канала ограничить — начинаются тормоза. Завтра доделаю код, выложу здесь, и параллельно буду гошку ковырять, возможно у нее многопоточность получше растовой будет.
0
Еще обнаружил что если поставить больше 6 потоков то начинает дико тупить. При 12 потоках 7 секунд выполняется. Явно что-то не так
0
Так и должно быть. В идеале тредов не более количества ядер. Дальше ОС начинает переключать контекст, а это дорого. Поэтому зеленые потоки / горутины — это все больше абстракция для программистов, чем реальное ускорение. Железо рулит.
0
Сейчас буфера каналов резиновые (в гошке они фиксированного размера). То есть память постоянно течет, и это плохо. Если в расте сделать их фиксированными, начинаются тормоза, видимо из-за мьютексов, которые зашиты внутрь синхронных каналов. Пока думаю как побороть.
0
Сделал без регулярки с учетом скобок внутри кавычек и экранирования: gist.github.com/tsukanov-as/d1532303e16d757e3fa9e70ccb21d64b

Удивительно, но даже так LuaJit быстрее Rust в одном потоке (1.6 sec).

Было бы здорово, если бы Вы собрали LuaJit у себя и сделали замер в своих условиях.

Собирается легко из репозитория: github.com/LuaJIT/LuaJIT
Библиотеку cjson можно поставить через менеджер пакетов: luarocks.org/modules/openresty/lua-cjson
0
Приблизительно понятно в чем дело. Я использую промежуточную структуру, и возвращаю ее из функции по значению, то есть память выделяется заново. Потом я строки заталкиваю в результат, тоже по значению. Это проблема всех статических языков, почему порой код Java быстрей кода C++ — в интерпретаторах строка всегда в единственном экземпляре, а в C++ это на усмотрение программиста. Я смогу догнать, если уберу дополнительный слой, и поднапрягусь со ссылками. Собственно, поэтому у Rust только одно преимущество — экономия памяти, и то если не используем тредов. На луа библиотеку не напишешь. Хотя, на JS я ваш код уже не догоню :)
0
Мне нравится в Lua насколько мало нужно усилий, ума и знаний, чтобы написать эффективный код.
Язык то детский совсем.

ps Интересно насколько смогут еще ускорить этот код те, кто разбирается в LuaJit
0
После столь поразительных результатов решил почитать про LuaJIT… Там и вправду под капотом «магия», очень заинтересовало что разогрев очень быстрый, обычно всё наоборот.
0
Неужели существует то, чего нет в V8 и джаве, вроде эти ребята собаку съели на Jit, у них там и фоновая компиляция в натив, и черте что?
+1
Ну примерно так было в 11 году, когда я только знакомился с Lua: habr.com/ru/post/113804

LuaJit уже давненько остановился в развитии судя по репозиторию (там очень маленькая активность), но как не странно до сих пор способен конкурировать с монстрами индустрии.
0
Меня вот больше удивляет что интерпретатор Lua 5.3.5, который целиком 300Кб, похоже способен конкурировать в некоторых задачах со слоном Swift. И код значительно проще и работает эффективнее.
0
Свифт вообще как мы выяснили, это просто фасад для обжектси с NS-объектами. V8 тоже очень шустро работает, и сфер применения больше, и классы с модулями есть, и даже тайпскрипт. Я бы ноду не списывал пока.
0
Теоретически должен, но статус реализации в текущей версии мне неизвестен, по ощущениям строки скорее да, остальное скорее нет.
0
Тогда хана :) Не нашел я нормального потокового парсера на Rust, хотя потом товарищи подсказали, что на serde можно сделать.
0
Да я уже понял что и машина получше, у меня старейший 2-х ядерный ноут 2 x Intel® Celeron® CPU 1005M @ 1.90GHz.
Прикол в том, что лучшая производительность получилась с пулом из 4-х тредов (не считая главного, который файл по байтам разбирает).
0
Так запилите статью на эту тему, на хабре без статьи у вас невозможна карма больше 4, не сможете голосовать и проч. Если владеете гошкой и луа — сошлитесь на мой код, и на своей машине проведите замеры, и табличка сравнения.
Только научитесь память правильно смотреть, я не знаю винды, но надо изучить эту тему, чтобы отдельно куча, отдельно стек.
0
Ой, у меня маловато ума чтоб на такие темы грамотно писать.
Я обычный одноэсник из деревни. Ни на Lua ни на Go не пишу. Играюсь только раз в пол года.
0
На гошке вакансий достаточно, даже в Краснодаре видел. И почти везде можно удаленно. Я с бухгалтерского софта уже соскочил, надеюсь не вернусь обратно к кактусам и фактурам.
0
Хочу соскочить на Go, но чего-то жду пока )
Смелости наверно не хватает.
0
Мне честно непонятен пока круг работодателей для гошки, но точно понятно что наступают времена, когда лучше иметь потенциальную возможность работать на заграницу, ибо внутри страны может остаться только пищевка и рынки.
0

Можно попробовать managed-языки типа того же C#. Могу как-нибудь запилить вариант, если интересно.

0
C# это вообще безумно интересно, не верю что он захватит мир, но по числу вакансий вполне себе живет. Думаю будет как lua.
0

Короче, вроде как работает, на примере который вы скинули отрабатывает. Сделал максимально чисто, без хаков и ручного парсинга. Скорость должна быть сравнительно печальной, можно ускорить если вместо динамического JObject использовать свою десериализацию.


https://github.com/Pzixel/HabrIndalidJsonTest

+1

Наткнулся на статью, удалось написать на go без рефлексии и относительно типобезопасно (156 строк), получилось 559ms.
https://gist.github.com/ernado/9d6bb51ab91887cc02a9540df4fb535d


total debtors: 2
total time: 559.910525ms
Первая коллекторская, Рога и копыта, Шестерочка:
        123, 2128506, 234, 456, 788, 789
        910000000
Казачий спас, Святой престол:
        234567, 345678, 666
        433200000

Для сравнения: предыдущая версия на Go показала 2.40s, ваша версия на Rust: 1.51s.
Код можно упростить и структурировать, но основная идея — если парсить потоково и сразу агрегировать, то получается быстро.

+1
Класс, я тоже сторонник делать все в одном цикле, и по возможности без библиотек — аллокаций нет, лишних вызовов нет, расход памяти ноль. И распараллелить это вполне реально, принимая входящий поток данных, и разбрасывая объекты по разным нитям. Даже в SQL при обработке миллиардов записей переходил на цикл, ибо запросы давали непредсказуемое время, даже статью об этом написал, но никого особо так кодить не вдохновило :)
+1

Я потом попробовал наивно распараллелить — получилось медленнее (~770ms), оверхед на рантайм высокий (в основном это блокировки и шедулинг горутин), ну и сложность решения выше даже при наивном подходе.


Этот оверхед можно смягчать, вводя нетривиальную модель воркеров как в fasthttp, но это уже сильно сложнее и в ~200 строчек кода не влезает.

0
Я распараллеливал на расте вполне успешно, выигрыш как ожидалось в разы, только накосячил немного при слиянии результата. Синхронные каналы (фиксированной емкости) действительно подтормаживают, но это и понятно — там в основе мьютекс. Асинхронные каналы они lock-free, но в продакшн такое нельзя — память утекает бесконтрольно.
github.com/epishman/habr_samples/blob/master/jsonparse/main.rs
PS
Важно — при использовании мьютекса важен небольшой sleep, иначе можно другой поток до смерти задрочить блокировками.
0
Я распараллеливал на расте вполне успешно, выигрыш как ожидалось в разы, только накосячил немного при слиянии результата.

Распараллеливание на расте было крайне неуспешным в связи с неадекватным оверхедом на само распараллеливание.
0
Я потом попробовал наивно распараллелить — получилось медленнее (~770ms), оверхед на рантайм высокий (в основном это блокировки и шедулинг горутин), ну и сложность решения выше даже при наивном подходе.

Сложно как-то у вас всё.

Я на базе С++-версии написал такой бенчмарк
//https://github.com/jarro2783/cxxopts.git
//https://github.com/cameron314/concurrentqueue.git
#include <filesystem>
#include <sys/mman.h>
#include <fcntl.h>
#include <string>
#include <vector>
#include <boost/algorithm/string/join.hpp>
#include <algorithm>
#include <cxxopts.hpp>
#include <concurrentqueue/concurrentqueue.h>
#include <future>


moodycamel::ConcurrentQueue<std::string_view> q;

struct config {
  static inline size_t bulk = 128;
  static inline size_t concurrency = std::min(4u, std::thread::hardware_concurrency());
};

auto work() {
  std::vector<std::string_view> pool(config::bulk);
  size_t size = 0;
  
  for(bool end_flag = false; !end_flag;) {
    auto n = q.try_dequeue_bulk(begin(pool), config::bulk);
    for(size_t it = 0; it < n; ++it) {
      auto & str = pool[it];
      if((end_flag = str.empty())) continue;
      size += str.size();
    }
    
    if(end_flag) q.enqueue({});
  }
  return size;
}


auto parse(int argc, char* argv[]) {
  cxxopts::Options options{*argv};
  options.add_options()
    ("b", "", cxxopts::value(config::bulk))
    ("c", "threads", cxxopts::value(config::concurrency))
    ("f", "file path", cxxopts::value<std::string>());
  auto pr = options.parse(argc, argv);
  if(pr.count("f") == 1) return pr["f"].as<std::string>();
  printf("%s\n", options.help().c_str());
  exit(0);
}


int main(int argc, char * argv[]) {
  std::filesystem::path path = parse(argc, argv);
  auto file_size = std::filesystem::file_size(path);
  auto file_data = (const char *)mmap(nullptr, file_size + 4096, PROT_READ, MAP_POPULATE|MAP_PRIVATE|MAP_FILE, open(path.c_str(), O_RDONLY), 0);
  std::string_view file{file_data, file_size};
    
  auto next = [it = file]() mutable {
    auto p = it.data();
    const char * begin = nullptr;
    size_t braces = 0;
    while(true) {
      switch(p = strpbrk(p, "{}"); p ? *p : 0) {
        case '{': if(!braces++) begin = p; ++p; break;
        case '}': ++p; if(!--braces) {
          it = it.substr(size_t(p - it.begin()));
          return std::string_view{begin, size_t(p - begin)};
        } break;
        default: it = it.substr(it.size()); return std::string_view{};
      }
    };
  };
  
  std::vector<std::future<size_t>> tp(config::concurrency);
  for(auto & f: tp) f = std::async(std::launch::async, work);
  
  std::vector<std::string_view> pool;
  auto max = config::bulk * config::concurrency;
  pool.reserve(max);
  while(true) {
    auto str = next();
    if(pool.size() == max || str.empty()) {
      q.enqueue_bulk(begin(pool), pool.size());
      pool.clear();
    }
    if(str.empty()) {
      q.enqueue({});
      break;
    }
    pool.emplace_back(str);    
  }
  std::vector<std::string> all;
  std::transform(begin(tp), end(tp), std::back_inserter(all), [](auto & f) {
    return std::to_string(f.get());
  });
  assert(all.size() == config::concurrency);
  fprintf(stderr, "{%s}\n", boost::algorithm::join(all, ", ").c_str());
  
}



Оно парсит(при помощи алгоритма автора) на подстроки и раскидывает подстроки по потокам.

//1 поток(на самом деле 2)
{93000000}
real 0m0,041s
user 0m0,054s
sys 0m0,012s
//4 потока(на самом деле 5)
{23247900, 23247942, 23256193, 23247965}
real 0m0,042s
user 0m0,140s
sys 0m0,010s


Там лям объектов объектов. Их без проблем можно рассылать по потокам по тысячи штук, а это всего тысяча операций. Это ничто даже для системных механизмов синхронизации, не говоря уже о юзерспейсных.

0
А смысл иметь тысячу нитей, все мои замеры показывают — максимальный выигрыш когда количество потоков немного больше количества ядер (максимум в 2 разе), дальше начинаются тормоза, особенно если не использовать lock-free каналы (которых кстати как я понял в Go нет).
+1
А смысл иметь тысячу нитей

Я не говорил о каких-то «нитях». Про нити там:

рассылать по потокам

Всё что дальше — к потокам отношения не имеет, а имеет отношение к «их».

Отвечу сразу на:
В чем заключался оверхед?

Это уже обсуждалось. Там ниже есть бенчмарки С++-версии и раст-версии и оверхед там очевиден(там же обсуждался оверхед с red75prim). Многопоточная раст-версия работает на одном потоке намного медленнее однопоточной — это и есть оверхед.

Для понимания темы я советую взять и написать аналог моего бенчмарка и сравнить результаты. Он читает файл, берёт ваш алгоритм по выделению из строки объектов и делит строку на подстроки(вот они тут: github.com/epishman/habr_samples/blob/master/jsonparse/main.rs#L187). Далее он посылает эти подстроки в воркеры(которые потоки).

В рамках бенчмарка воркер не просто считает длину этой подстроки и складывает внутри себя. Далее бенчмарк выводит суммарной кол-во байт «переданное» в каждый воркер(это показанные мною 1/4 цифры).

github.com/epishman/habr_samples/blob/master/jsonparse/main.rs#L284 — вот то место, у вас там o является подстрокой. Нужно взять и сложить все длинны входящих слайсов для каждого элемента массива: github.com/epishman/habr_samples/blob/master/jsonparse/main.rs#L141

0
Понял, спасибо. Но такую же проблему мы видим в Go, что как-бы намекает, что к производительности ++ никто так и не подобрался. Но тогда это смерть раста :)
0
real 0m0,533s
user 0m0,519s
sys 0m0,014s

//C++
real 0m0,536s
user 0m0,496s
sys 0m0,040s

//C++ + pgo
real 0m0,462s
user 0m0,423s
sys 0m0,039s

Т.е. оно эквивалентно С++-версии.

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

А если ещё и json(текст) убогий выпилить.
0
Интерестно было бы сравнить с Crystal. Тоже компилируется для LLVM, приятный синтаксис. Раньше делал на нем процессинг CSV файлов, отрабатывал шустро.
0
Как я понял, для настоящего парсинга нужна многопоточка. В случае с JSON/XML это оправдано, так как скорость чтения из потока/файла сильно больше скорости остальной обработки. Проблема, если важна последовательность обработки, например финансовых документов, которые зависят друг от друга — в этом случае приходится часть документов кэшировать в памяти, что приводит к ее непредсказуемому расходу.
+1
Написал С++-версию. Можете добавить к сравнению.

Код
#include <rapidjson/document.h>
#include <filesystem>
#include <sys/mman.h>
#include <fcntl.h>
#include <unordered_map>
#include <charconv>
#include <string>
#include <unordered_set>
#include <list>
#include <array>
#include <vector>
#include <boost/algorithm/string/join.hpp>


struct rec_t {
  std::string company;
  std::vector<std::string> phones;
  double debt;
};

struct debtor_t {
  static inline std::unordered_map<std::string, debtor_t &> by_phone_index;
  static inline std::list<debtor_t> all;
  std::unordered_set<std::string> companies;
  std::unordered_set<std::string> phones;
  double debt;
};

void process_object(rec_t & rec) {
  
  auto & index = debtor_t::by_phone_index;
  auto op = [&](debtor_t & d) {
    d.companies.insert(std::move(rec.company));
    for(const auto & phone: rec.phones) {
      d.phones.insert(phone);
//       if(!index.contains(phone))
      if(index.find(phone) == end(debtor_t::by_phone_index))
        index.emplace(phone, d);
    }
    d.debt += rec.debt;
  };
  
  auto di = end(index);
  for(const auto & phone: rec.phones) {
    di = index.find(phone);
    if(di != end(index)) break;    
  }
  op((di != end(index)) ? di->second : debtor_t::all.emplace_back());
}



rec_t & extract_data(rapidjson::Document & doc) {
  static rec_t rec;
  rec.phones.clear();
  rec.company.clear();
  rec.debt = 0;
  
  for(auto & x: doc.GetObject()) {
    std::string_view key{x.name.GetString(), x.name.GetStringLength()};
    auto & value = x.value;
    
    auto val2str = [](auto & value) -> std::string {
      std::array<char, 32> mem;  
      
      switch(value.GetType()) {
        case rapidjson::kStringType: return {value.GetString(), value.GetStringLength()};
        case rapidjson::kNumberType: {
          auto [endp, _] = std::to_chars(begin(mem), end(mem), value.GetUint64());
          return {begin(mem), endp};
        }
        default: return {};
      }
    };
    
    auto val2num = [](auto & value) -> double {
      switch(value.GetType()) {
        case rapidjson::kStringType: return strtod(value.GetString(), nullptr);
        case rapidjson::kNumberType: return value.GetDouble();
        default: return {};
      }
    };
    
    auto push_phone = [&](auto & value) {
      if(auto phone = val2str(value);!phone.empty())
        rec.phones.emplace_back(phone);
    };
    
    if(key == "company") {
      if(value.IsString()) {
        rec.company = {value.GetString(), value.GetStringLength()};
      } else if(value.IsObject()) {
        auto name = value.GetObject().FindMember("name");
        if(name != value.GetObject().MemberEnd()) rec.company = val2str(name->value);
      }
    } else if(key == "phone") {
      push_phone(value);
    } else if(key == "phones") {
      if(value.IsArray()) for(auto & x: value.GetArray()) push_phone(x);
      push_phone(value);
    } else if(key == "debt") rec.debt = val2num(value);
  }
  
  return rec;  
}

int main(int argc, char * argv[]) {
  if(argc < 2) return -1;
  std::filesystem::path path = argv[1];
  
  auto file_size = std::filesystem::file_size(path);
  auto file_data = (const char *)mmap(nullptr, file_size + 4096, PROT_READ, MAP_POPULATE|MAP_PRIVATE|MAP_FILE, open(path.c_str(), O_RDONLY), 0);
  std::string_view file{file_data, file_size};
  
  auto it = file;
  
  auto next = [&] {
    auto p = it.data();
    const char * begin = nullptr;
    size_t braces = 0;
    while(true) {
      switch(p = strpbrk(p, "{}"); p ? *p : 0) {
        case '{': if(!braces++) begin = p; ++p; break;
        case '}': ++p; if(!--braces) {
          it = it.substr(size_t(p - it.begin()));
          return std::string_view{begin, size_t(p - begin)};
        } break;
        default: it = it.substr(it.size()); return std::string_view{};
      }
    };
  };
  
  rapidjson::Document doc;
  
  
  while(true) {
    auto str = next();
    if(str.empty()) break;
    doc.Parse(data(str), size(str));
    process_object(extract_data(doc));
  }
  
  for(size_t it = 0; auto & d: debtor_t::all) {
    printf("-------------------------------\n");
    printf("#%lu: debt: %f\n", it++, d.debt);
    using boost::algorithm::join;
    printf("companies: {%s}\nphones: {%s}\n", join(d.companies, ",").c_str(), join(d.phones, ",").c_str());
  }
}

0
Как это компилить, я не владею крестами, и у меня линукс.
PS
Уже столько вариантов люди накидали, на статью тянет. Один товарищ даже выиграл гонку на lua. Сравнение раста и плюсов это очень холиварная тема, завтра изучу что нужно на машину поставить чтоб Ваш пример собрать.
0
Там всё header-only. github.com/Tencent/rapidjson — нужно куда-то клонировать и добавить путь до rapidjson/include

git clone https://github.com/Tencent/rapidjson.git
g++ main.cpp -std=c++2a -Irapidjson/include -Ofast -march=native -fwhole-program
$ time ./a.out 1.json
-------------------------------
#0: debt: 910000000.000000
companies: {Шестерочка, Рога и копыта, Первая коллекторская}
phones: {234, 789, 123, 456, 788, 2128506}
-------------------------------
#1: debt: 433200000.000000
companies: {Казачий спас, Святой престол}
phones: {345678, 666, 234567}

real 0m0,545s
user 0m0,498s
sys 0m0,047s


В любом дистрибутиве boost уже должен стоять. Если нет:

Вот версия без буста
#include <rapidjson/document.h>
#include <filesystem>
#include <sys/mman.h>
#include <fcntl.h>
#include <unordered_map>
#include <charconv>
#include <string>
#include <unordered_set>
#include <list>
#include <array>
#include <vector>

struct rec_t {
  std::string company;
  std::vector<std::string> phones;
  double debt;
};

struct debtor_t {
  static inline std::unordered_map<std::string, debtor_t &> by_phone_index;
  static inline std::list<debtor_t> all;
  std::unordered_set<std::string> companies;
  std::unordered_set<std::string> phones;
  double debt;
};

void process_object(rec_t & rec) {
  
  auto & index = debtor_t::by_phone_index;
  auto op = [&](debtor_t & d) {
    d.companies.insert(std::move(rec.company));
    for(const auto & phone: rec.phones) {
      d.phones.insert(phone);
//       if(!index.contains(phone))
      if(index.find(phone) == end(debtor_t::by_phone_index))
        index.emplace(phone, d);
    }
    d.debt += rec.debt;
  };
  
  auto di = end(index);
  for(const auto & phone: rec.phones) {
    di = index.find(phone);
    if(di != end(index)) break;    
  }
  op((di != end(index)) ? di->second : debtor_t::all.emplace_back());
}



rec_t & extract_data(rapidjson::Document & doc) {
  static rec_t rec;
  rec.phones.clear();
  rec.company.clear();
  rec.debt = 0;
  
  for(auto & x: doc.GetObject()) {
    std::string_view key{x.name.GetString(), x.name.GetStringLength()};
    auto & value = x.value;
    
    auto val2str = [](auto & value) -> std::string {
      std::array<char, 32> mem;  
      
      switch(value.GetType()) {
        case rapidjson::kStringType: return {value.GetString(), value.GetStringLength()};
        case rapidjson::kNumberType: {
          auto [endp, _] = std::to_chars(begin(mem), end(mem), value.GetUint64());
          return {begin(mem), endp};
        }
        default: return {};
      }
    };
    
    auto val2num = [](auto & value) -> double {
      switch(value.GetType()) {
        case rapidjson::kStringType: return strtod(value.GetString(), nullptr);
        case rapidjson::kNumberType: return value.GetDouble();
        default: return {};
      }
    };
    
    auto push_phone = [&](auto & value) {
      if(auto phone = val2str(value);!phone.empty())
        rec.phones.emplace_back(phone);
    };
    
    if(key == "company") {
      if(value.IsString()) {
        rec.company = {value.GetString(), value.GetStringLength()};
      } else if(value.IsObject()) {
        auto name = value.GetObject().FindMember("name");
        if(name != value.GetObject().MemberEnd()) rec.company = val2str(name->value);
      }
    } else if(key == "phone") {
      push_phone(value);
    } else if(key == "phones") {
      if(value.IsArray()) for(auto & x: value.GetArray()) push_phone(x);
      push_phone(value);
    } else if(key == "debt") rec.debt = val2num(value);
  }
  
  return rec;  
}

int main(int argc, char * argv[]) {
  if(argc < 2) return -1;
  std::filesystem::path path = argv[1];
  
  auto file_size = std::filesystem::file_size(path);
  auto file_data = (const char *)mmap(nullptr, file_size + 4096, PROT_READ, MAP_POPULATE|MAP_PRIVATE|MAP_FILE, open(path.c_str(), O_RDONLY), 0);
  std::string_view file{file_data, file_size};
  
  auto it = file;
  
  auto next = [&] {
    auto p = it.data();
    const char * begin = nullptr;
    size_t braces = 0;
    while(true) {
      switch(p = strpbrk(p, "{}"); p ? *p : 0) {
        case '{': if(!braces++) begin = p; ++p; break;
        case '}': ++p; if(!--braces) {
          it = it.substr(size_t(p - it.begin()));
          return std::string_view{begin, size_t(p - begin)};
        } break;
        default: it = it.substr(it.size()); return std::string_view{};
      }
    };
  };
  
  rapidjson::Document doc;
  
  
  while(true) {
    auto str = next();
    if(str.empty()) break;
    doc.Parse(data(str), size(str));
    process_object(extract_data(doc));
  }
  
  for(size_t it = 0; auto & d: debtor_t::all) {
    printf("-------------------------------\n");
    printf("#%lu: debt: %f\n", it++, d.debt);
    std::string companies, phones;
    for(auto & x: d.companies) companies += (x + ", ");
    for(auto & x: d.phones) phones += (x + ", ");
    if(companies.ends_with(", ")) companies.resize(companies.size() - 2);
    if(phones.ends_with(", ")) phones.resize(phones.size() - 2);
    printf("companies: {%s}\nphones: {%s}\n", companies.c_str(), phones.c_str());
  }
}



У меня эта версия(0.55 секунды) где-то в 3раза быстрее раста(1.6 секунды).

0
Ну вобще это не совсем честно делать memory map. Тогда и в когде на раст надо его использовать.
0
Ну вобще это не совсем честно делать memory map.

Он мало что даёт в этом случае.

Тогда и в когде на раст надо его использовать.

Предложите автору изменения. Я не знаю как это сделать.
0

Не собирается. g++-7 не знает -std=c++2a, на g++-8 вот это:


$ g++-8 --version
g++-8 (Ubuntu 8.3.0-6ubuntu1~18.04) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ g++-8 main.cpp -std=c++2a -I../rapidjson/include -Ofast -march=native -fwhole-program
main.cpp: In function ‘int main(int, char**)’:
main.cpp:141:30: error: found ‘:’ in nested-name-specifier, expected ‘::’
   for(size_t it = 0; auto & d: debtor_t::all) {
                              ^
                              ::
main.cpp:141:22: error: expected primary-expression before ‘auto’
   for(size_t it = 0; auto & d: debtor_t::all) {
                      ^~~~
main.cpp:141:21: error: expected ‘;’ before ‘auto’
   for(size_t it = 0; auto & d: debtor_t::all) {
                     ^~~~~
                     ;
main.cpp:141:22: error: expected primary-expression before ‘auto’
   for(size_t it = 0; auto & d: debtor_t::all) {
                      ^~~~
main.cpp:141:21: error: expected ‘)’ before ‘auto’
   for(size_t it = 0; auto & d: debtor_t::all) {
      ~              ^~~~~
                     )
main.cpp:141:29: error: ‘d’ has not been declared
   for(size_t it = 0; auto & d: debtor_t::all) {
                             ^
main.cpp:141:45: error: qualified-id in declaration before ‘)’ token
   for(size_t it = 0; auto & d: debtor_t::all) {
0

Если исправить эту ошибку вылезают ошибки с .ends_with(). Потом


/tmp/ccQErZlq.o: In function `main':
main.cpp:(.text.startup+0xa1): undefined reference to `std::filesystem::__cxx11::path::_M_split_cmpts()'
main.cpp:(.text.startup+0xab): undefined reference to `std::filesystem::file_size(std::filesystem::__cxx11::path const&)'
collect2: error: ld returned 1 exit status

И на этом я бросил.

0
Я бы не спешил считать Lua победителем. Нужна независимая проверка.
Может быть я ошибся. Не так померил или не то закодил или еще чего.
Плюс ко всему нужно учитывать что таки используется библиотека cjson, которая реализована на сишке.
+1
$ time luajit main.lua
done 1.241739 sec.
[{"debt":910000000,"companies":{"Шестерочка":true,"Рога и копыта":true,"Первая коллекторская":true},"phones":{"788":true,"456":true,"789":true,"2128506":true,"123":true,"234":true}},{"debt":433200000,"companies":{"Святой престол":true,"Казачий спас":true},"phones":{"234567":true,"666":true,"345678":true}}]

real 0m1,242s
user 0m1,173s
sys 0m0,069s


$ time ./target/release/habr -f 1.json
1.json:
PROCESSED: 1000000 objects in 1.609574904s, 0 errors found
-------------------------------
#0: debt: 910000000
companies: {"Первая коллекторская", "Шестерочка", "Рога и копыта"}
phones: {"788", "123", "456", "234", "2128506", "789"}
-------------------------------
#1: debt: 433200000
companies: {"Святой престол", "Казачий спас"}
phones: {"234567", "345678", "666"}

real 0m1,611s
user 0m1,599s
sys 0m0,010s


Топ 15 вызовов rust-версии
13.48% habr habr [.] habr::main
8.54% habr libc-2.29.so [.] _int_free
5.91% habr habr [.] serde_json::value::de::<impl serde::de::Deserialize<'de> for serde_json::value::Value>::deserialize
5.13% habr libc-2.29.so [.] malloc
4.89% habr libc-2.29.so [.] realloc
3.79% habr libc-2.29.so [.] _int_malloc
3.70% habr habr [.] <serde_json::read::SliceRead<'a> as serde_json::read::Read<'a>>::parse_str
3.28% habr habr [.] <std::collections::hash::map::DefaultHasher as core::hash::Hasher>::write
3.05% habr libc-2.29.so [.] _int_realloc
2.99% habr habr [.] <serde_json::value::de::<impl serde::de::Deserialize<'de> for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor<'de>>::visit_map
2.97% habr habr [.] std::collections::hash::table::make_hash
2.92% habr habr [.] core::str::run_utf8_validation
2.86% habr libc-2.29.so [.] __memmove_avx_unaligned_erms
2.80% habr libc-2.29.so [.] cfree@GLIBC_2.2.5
2.32% habr habr [.] <alloc::collections::btree::map::BTreeMap<K, V>>::insert
2.21% habr libc-2.29.so [.] __memcmp_avx2_movbe



rust по библиотекам
65,31% habr
34,07% libc-2.29.so
0,59% [unknown]
0,03% ld-2.29.so



Процент cjson во времени выполнения lua
22,67% cjson.so


0
Классно. То есть мы видим, что большинство времени уходит на алгоритм, а не на сторонние вызовы, а также на чтение файла. Как и предполагалось — маллоки на 3-м месте. В алгоритме ЛУА вроде чтение файла делается по другому, и там не создается промежуточная структура, то есть идет агрегация с колес, по сырым данным. Тут мне все понятно. Но оптимизировать код Rust я еще не дорос пока :)
+1

https://gist.github.com/red75prime/53111e066cb1a32721c461b24eadbe6e


Где-то в два раза ускорил Rust версию — использовал RawValue, чтобы избежать лишнего парсинга и копирования.


Версия с mmap: https://gist.github.com/red75prime/676ba0f8b8e830861679f03f6824cb77


У меня она работает немного медленнее.


тогда как правильнее было бы заморочиться со ссылками и временами жизни (хотя предполагаю, что умный компилятор сделал это за меня)

Нет, в Rust таких оптимизаций нет. Если написано to_string(), то будет создана новая строка и выделена память под неё.

0
Круто, но Вы еще и алгоритм хеширования поменяли, вроде в std он медленный.
PS
но можно писать to_owned(), применимо к любым слайсам, теоретически он мог бы в некоторых случаях повторно использовать память, но это не наш случай — у нас память это кусок большого буфер, который перетирается на следующей итерации.
PPSS
То есть, в сухом остатке, можно считать, что мы догнали C++? twinklede — я ничего не понимаю в плюсах, и даже вряд-ли смогу собрать тест. Материал тянет на статью, может кто напишет? :)
0
То есть, в сухом остатке, можно считать, что мы догнали C++?

Пока не знаю. Там похоже bleeding edge C++ — g++ 8.3.0 его не понимает, а новее в моей убунте нет.

0
То есть, в сухом остатке, можно считать, что мы догнали C++?

Нет, почти в 2раза медленнее.

Материал тянет на статью, может кто напишет? :)

Мой код откровенно позорный. Так же я не заниматься оптимизациями. Если вам нужно быстро читать/писать/считать — вы явно выбрали не тот формат хранения и алгоритм. Используйте базу данных для таких задач.

Я примерно предполагаю как ускорить С++-версию ещё раза в 2. Но если использовать бинарный, строгий формат данных — можно получить результат в разы быстрее текущих даже для самой базовой, не оптимизированной версии. Мы просто тратим силы впустую.

0
Так JSON и есть бинарный формат — все разделители байты, остальное в ut8, единственная проблема — нет отдельных делимиторов верхнего уровня, и нет блочности, поэтому распараллелить расчет можно только после первичного поточного парсинга.
PS
СУБД и рядом не стояли по производительности на сопоставимом железе, поэтому для бигдаты и не используют реляционку, банально не тянет. Все хранится в древовидных документах, и обрабатывается фулсканом.
+1
Так JSON и есть бинарный формат — все разделители байты, остальное в ut8, единственная проблема — нет отдельных делимиторов верхнего уровня, и нет блочности, поэтому распараллелить расчет можно только после первичного поточного парсинга.

Он не бинарный формат в том смысле в котором понимают бинарным форматы. Формально да, все форматы являются бинарными.

Основных отличий много. В бинарном формат строгий/однозначный, а значит в данных ненужно хранить структуру. У вас же оно хранится. Бинарный формат компактней. Ненужно ничего никуда преобразовывать(строки в числа, числа в строки и прочее). Подобные форматы заранее спроектированы таким образом, что-бы хранить данные в наиболее удобной для обработки вида. Про блочность вы уже сказали. Продолжать можно долго.

СУБД и рядом не стояли по производительности на сопоставимом железе, поэтому для бигдаты и не используют реляционку, банально не тянет.

Нужно сравнивать. К тому же БД не обязательно должна быть реляционной. Аналогично, сомневаюсь, что кто-то в бигдате использует json. Для хранения отдельных документов? Возможно. Но то отдельные документы. К тому по эти документам строятся внешние индексы/иерархия.
0
Я кстати много работал с объектами Javascript, и да, они быстрее в разы, если их хранить в документной базе в виде именно объектов JS. Даже если это IndexedDB с индексами. Но почему-то нет стандарта сериализации, подобного джаве, и все пользуют убогий Json/Xml.
0
Так разве JSON не является стандартом сериализации для Javascript?
0
Ну так да, но в базе похоже объекты хранятся в более быстром виде, хотя не проверял.
0
В Сўифт есть сериализация в бинарник, и если писать на Сўифт и клиент, и сервер, то, я полагаю, без проблем можно будет и бинарные данные пересылать через сеть. Подобно Джаве и получится, насколько я понял.
Но, да, кросс‐языкового стандарта нет.
0
У меня 1.3 сек. против 1.6 сек. на LuaJit
Правда оно не переварило файл со скобками внутри строк.

ps Теперь интересно быстрее плюсы или нет. Я их на винде не смог собрать
0
real 0m1,242s — luajit
real 0m1,611s — rust(старый)
real 0m0,963s — rust(новый)
У вас:
1.6 — luajit
1.3 — rust(новый)

1.288 — коэффициент между нами.
Если поделить ваши результаты на коэффициент, то мы получим 1sec(для rust новый). Что соотносится с моими измерениями.

Версия С++ у вас должна выполнятся примерно 0.71sec. Rust(старый) должна выполняться примерно 2sec.

Я их на винде не смог собрать

Я адаптирую код под gcc8.
0
Версия без mmap
$ time ./target/release/habr -f 1.json
1.json:
PROCESSED: 1000000 objects in 961.843328ms, 0 errors found
-------------------------------
#0: debt: 910000000
companies: {"Рога и копыта", "Первая коллекторская", "Шестерочка"}
phones: {"789", "788", "123", "234", "2128506", "456"}
-------------------------------
#1: debt: 433200000
companies: {"Казачий спас", "Святой престол"}
phones: {"345678", "666", "234567"}

real 0m0,963s
user 0m0,958s
sys 0m0,005s



Версия с mmap
$ time ./target/release/habr -f 1.json
1.json:
PROCESSED: 1000000 objects in 998.797718ms, 0 errors found
-------------------------------
#0: debt: 910000000
companies: {"Рога и копыта", "Первая коллекторская", "Шестерочка"}
phones: {"789", "788", "123", "234", "2128506", "456"}
-------------------------------
#1: debt: 433200000
companies: {"Казачий спас", "Святой престол"}
phones: {"345678", "666", "234567"}

real 0m1,000s
user 0m0,994s
sys 0m0,006s



У меня она работает немного медленнее.

mmap(NULL, 99000002, PROT_READ, MAP_SHARED, 3, 0) = 0x7fd176971000
Попробуйте изменить флажки на «как у меня». Здесь mmap нужен не столько для скорости, а сколько для уменьшения потребления памяти. PROT_READ + MAP_POPULATE он сразу мапит пейджкеш в юзерспейс. Без каких-либо аллокация и копирования. В данном случае файл не используется весь, а читается последовательно. Эффект с zero-copy и отсутствия раздувания памяти будет заметен мало.

0

Да, там можно много чего ускорить. Отказаться от валидации utf-8 как в С++ версии (~10%), использовать платформо-специфичные оптимизации для mmap (я использовал кросплатформенный memmap), использовать serde_json в режиме парсинга, а не десериализации и т.п. Может быть позже займусь.

0

https://gist.github.com/red75prime/b19c5cb369ee12a1ccfe8b6649b06647


Добавил параллелизм (воспользовался вашим merge_result), исключил лишнее выделение памяти при вставках в HashSet, HashMap, добавил обработку кавычек.


Результаты такие (~650ms C++, ~260ms Rust):


red75:/mnt/d/git/habr1$ time target/release/habr1 -f 1.json
[...]
real    0m0.270s
user    0m1.219s
sys     0m0.047s
red75:/mnt/d/git/habr1$ time ../habr2_cpp/a.out 1.json
[...]
real    0m0.666s
user    0m0.422s
sys     0m0.172s
red75:/mnt/d/git/habr1$ time ../habr2_cpp/a.out 1.json
[...]
real    0m0.653s
user    0m0.453s
sys     0m0.188s
red75:/mnt/d/git/habr1$ time target/release/habr1 -f 1.json
[...]
real    0m0.250s
user    0m1.156s
sys     0m0.031s
red75:/mnt/d/git/habr1$ time target/release/habr1 -f 1.json
[...]
real    0m0.269s
user    0m1.219s
sys     0m0.063s
red75:/mnt/d/git/habr1$ time ../habr2_cpp/a.out 1.json
[...]
real    0m0.657s
user    0m0.484s
sys     0m0.156s

red75:/mnt/d/git/habr1$ /usr/bin/time -v target/release/habr1 -f 1.json
[...]
        Command being timed: "target/release/habr1 -f 1.json"
        User time (seconds): 1.15
        System time (seconds): 0.04
        Percent of CPU this job got: 464%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.25
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 98360
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 24697
        Voluntary context switches: 0
        Involuntary context switches: 0
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0
red75:/mnt/d/git/habr1$ /usr/bin/time -v ../habr2_cpp/a.out 1.json
[...]
        Command being timed: "../habr2_cpp/a.out 1.json"
        User time (seconds): 0.48
        System time (seconds): 0.15
        Percent of CPU this job got: 97%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.65
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 266076
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 66704
        Voluntary context switches: 0
        Involuntary context switches: 0
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0
0
У меня два замечания. Оно работает неправильно(как минимум оно не читает более чем 4 телефона). Зачем вы сравниваете:
User time (seconds): 1.15
User time (seconds): 0.48

Какой в этом смысл?
0

Хм. На каком входе не читает?


red75:/mnt/d/git/habr1$ cat 2.json
[
    {"company":"Первая коллекторская",  "debt": 1200,   "phones": [2128506, 456, 789, "1", 2, 3, 4]},
]
red75:/mnt/d/git/habr1$ time target/release/habr1 -f 2.json
2.json:
PROCESSED: 1 objects in 625.2µs, 0 errors found
-------------------------------
#0: debt: 1200
companies: {"Первая коллекторская"}
phones: {"789", "3", "4", "2128506", "456", "1", "2"}

real    0m0.007s
user    0m0.000s
sys     0m0.000s

/usr/bin/time там для измерения потребления памяти. Остальное надо было убрать из вывода.

+1
Хм. На каком входе не читает?

Сравнение
[
{"company":"Рога и копыта", "debt": 800, "phones": [123, 234, 456]},
{"company":"Первая коллекторская", "debt": 1200, "phones": [2128506, 456, 789, 123, 412, 12412]},
{"company":"Святой престол", "debt": "666", "phones": 666},
{"company": "Казачий спас", "debt": 1500, "phones": [234567, "345678"], "phone": 666},
{"company": {"name": "Шестерочка"}, "debt": 2550, "phones": 788, "phone": 789},
]


2.json:
PROCESSED: 5 objects in 169.14µs, 0 errors found
-------------------------------
#0: debt: 4550
companies: {"Первая коллекторская", "Шестерочка", "Рога и копыта"}
phones: {"234", "12412", "123", "2128506", "788", "412", "789", "456"}
-------------------------------
#1: debt: 2166
companies: {"Святой престол", "Казачий спас"}
phones: {"234567", "345678", "666"}


Запустил и уже подумал, что ошибся. Но нет — не ошибся. Оно попросту выдаёт каждый раз разный результат.

2.json:
PROCESSED: 5 objects in 647.78µs, 0 errors found
-------------------------------
#0: debt: 2166
companies: {"Казачий спас", "Святой престол"}
phones: {"345678", "666", "234567"}
-------------------------------
#1: debt: 3750
companies: {"Шестерочка", "Первая коллекторская"}
phones: {"789", "412", "788", "123", "12412", "2128506", "456"}
-------------------------------
#2: debt: 800
companies: {"Рога и копыта"}
phones: {"123", "234", "456"}



/usr/bin/time там для измерения потребления памяти. Остальное надо было убрать из вывода.

Дело не в выводе.

$ time taskset -c 0 ./target/release/habr -f 1.json
1.json:
PROCESSED: 1000000 objects in 825.626717ms, 0 errors found
-------------------------------
#0: debt: 910000000
companies: {"Рога и копыта", "Первая коллекторская", "Шестерочка"}
phones: {"789", "788", "123", "234", "2128506", "456"}
-------------------------------
#1: debt: 433200000
companies: {"Казачий спас", "Святой престол"}
phones: {"345678", "666", "234567"}

real 0m0,828s
user 0m0,790s
sys 0m0,038s


А в этом. Какой смысл сравнивать однопоточную и многопоточную версию? Сравнивайте однопоточные. Хотите сравнить многопоточные? Сравнивайте их.

Да и сравнение идёт крайне наивно, ведь нужно учитывать ещё и cputime. В условиях реального применения всегда будут желающие потратить cputime.
+1

Понял. Алгоритм мёржа неверный. А однопоточная версия в этом варианте будет медленнее из-за валидации utf-8, mmap'а и другого подхода к парсингу. Много переделывать надо. Может быть позже.


К слову, то, насколько просто можно распараллелить програму, тоже играет свою роль.

0
К слову, то, насколько просто можно распараллелить програму, тоже играет свою роль.

Ещё важнее то, насколько просто написать оптимальную(если мы говорим про оптимальные программы) программу. В одном случае берётся стандартная библиотека и просто пишется код «как попало» без каких-то попыток что-то оптимизировать. Единственная функция, которая там написана хоть как-то адекватно — это main::next. В другом случае требуются замена стандартных функций, множество хаков, тюнинга библиотек и прочего. Но даже этого недостаточно.

На тему распараллеливания. Тут всё куда сложнее. В подавляющем большинстве случаев попытка распараллелить задачу малоэффективна, либо попросту бессмысленна. Подобные мы видим и тут.

В данном случае распараллеливание достигается за счёт запуска отдельных инстансов задачи. Но проблема никуда не уходит. Трупут каждого потока так и остался лишь чуть большим от 0.5 от С++.

Запустить задачи в параллель везде и всюду так же просто, особенно используя готовые библиотеки для организации пайпов. Но в данном случае, как и в большинстве других, всё упирается в трупут парсинг+process_object. И от этого никуда не уйти.

Если задача ~линейно параллелится, то cputime не должно расти. Если же оно растёт — что-то делается не так.
0
Но проблема никуда не уходит. Трупут каждого потока так и остался лишь чуть большим от 0.5 от С++.

Раст, как и С++, предполагает, что программист знает что делает. Если написано &str — значит программист хочет валидный utf-8, что предполагает обязательную валидацию пользовательского ввода. Догадаться, что программист хочет максимально быстрый код без валидации, Раст, естественно, не может.


serde ориентирован на десериализацию в strongly typed персистентные структуры, а не на потоковый парсинг. Впрочем, библиотеки для максимально быстрого потокового парсинга JSON кажется ещё нет. Вот это — проблема, да.


Если задача ~линейно параллелится

Эту задачу нетривиально распараллелить линейно. Поэтому приходится тратить время на раскидывание миллиона объектов по нескольким потокам (оверхед около 300нс на объект, но накапливается), вместо того, чтобы разбить файл на несколько частей и отдать каждую своему потоку. Скажем есть у нас позиция в середине JSON, как вы определите в общем случае откуда начинается следующий объект? Придётся писать особый вариант парсинга со сканированием назад.

-1
Раст, как и С++, предполагает, что программист знает что делает. Если написано &str — значит программист хочет валидный utf-8, что предполагает обязательную валидацию пользовательского ввода. Догадаться, что программист хочет максимально быстрый код, Раст, естественно, не может.

Не совсем понимаю причём тут пользовательский ввод, когда читаются бинарные данные из файла. Если мне понадобиться валидация utf-8 — я вопрошу её явно. Раст ведь декларирует zero-cost-абстракции?

Так же, я говорил об ином. Для того, что-бы догнать наивный, кое-как написанный С++-код приходится что-то оптимизировать, хакать, страдать. И даже всего этого недостаточно.

serde ориентирован на десериализацию в strongly typed персистентные структуры, а не на потоковый парсинг.

У меня используется парсер в dom-режиме. В sax-режиме этот парсер куда быстрее, но до «максимально» и даже «быстрого» ему очень далеко. Не вижу смысла заниматься оптимизацией изначального тупикового подхода.

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

Всё это не имеет отношения к проблеме. У вас базовая логика почти в 2раза медленнее. Это значит, что абсолютно неважно какой там оверхед и как сложно что-то распараллелить.

Есть базовое обстоятельство. Трупут парсинга объекта и его обработки почти в два раза ниже. В этот самый трупут всё и упирается. На сколько угодно потоков этот трупут можно множить, но он останется лишь в районе 0.5. Ведь у С++ есть столько же потоков.

Это просто фокус. Работает он следующим образом. Есть трупут на одном потоке у С++ — это 1.0. Есть раст, у него трупут на потоке 0.5. Есть некий коэффициент(какой угодно, но положительный). Из этого следует, что добавляя потоки(один, два, десять) — мы когда-нибудь дойдём до этой единице.

Если же взять одинаковое кол-во потоков, одинаковый коэффициент роста за поток, то первый случай будет всегда быстрее. И быстрее в те самые 2 раза.
+2
Раст ведь декларирует zero-cost-абстракции?

И это zero-cost. Чтобы получить гарантированно валидный String из неконтролируемых внешних данных, нужно эти данные валидировать. Если программист знает, что данные точно валидны, он пишет unsafe { String::from_utf8_unchecked(...) } и получает UB, если данные оказываются не валидны. Или использует Vec<u8>, если нужно работать с любыми данными. Как я и говорил, нужно знать, что делаешь. Здесь бы больше подошёл Vec<u8> (возможность повреждения файла всё-таки исключать нельзя), но код начинал писать не я.


У вас базовая логика почти в 2раза медленнее.

Это не базовая логика. Это — 1) валидация utf-8, 2) моя ошибка с десериализацией Cow (значение всегда копировалось), 3) оверхед на работу с потоками.


Вот тут я всё это убрал: https://gist.github.com/red75prime/7b2f5cb45fae81a44ac9451ae9dece00


Заодно добавил кастомный аллокатор, чтобы немного отыграть проигрыш на флагах mmap. На моей машине версия с аллокатором обгоняет ваш код на 70ms, без аллокатора на 50ms.

0

ДОПОЛНЕНИЕ: Поправил баг (ну не работал я с serde никогда в zero-copy режиме), время выполнения поднялось на 50ms

0
Еще кстаи можно contains_key земенить на entry и уменьшить расчет хеша.
0

Не, там смысл в том, что сначала используем ссылку, а потом, если надо, делаем to_owned(). entry() требует owned значение.

0

ДОПОЛНЕНИЕ2: если убрать проверку на '{}' внутри строк, как в С++ коде, c которым я сравниваю, то время выполнения падает на 50ms.

-5
Поправил баг (ну не работал я с serde никогда в zero-copy режиме), время выполнения поднялось на 50ms

Я в очередной раз спрошу. Зачем? История аналогичная потоком. Вы пытаетесь похакать, добавить jemalloc, выкинуть стандартную библиотеку, добавить «zero-copy режиме». И все эти ухищрения нужны для того, что-бы ещё даже не догнать С++-версию без всего это.

Зачем вы сравнивали однопоточную и многопоточную версию? Думали, что я не добавлю многопоточную? Зачем вы занимаетесь этим шаманством? Вы думаете, я не добавлю его в С++-код? К чему эта пыль в глаза?
+2

Не понял. В статье представлена достаточно наивная реализация решения. Производительность Rust кода теоретически не должна отличаться от C++. Я разбираюсь почему она отличается. Какое ещё шаманство?


Думали, что я не добавлю многопоточную?

Эм, а что и многопоточная версия есть?


К чему эта пыль в глаза?

У вас всё в порядке? Обычное желание выжать максимум из кода. Не надо придумывать какие-то странные мотивации.

-3
В статье представлена достаточно наивная реализация решения.

Я предоставил такую же наивную реализацию на С++.

Производительность Rust кода теоретически не должна отличаться от C++.

Нужно сравнивать равные решения, иначе это всё не имеет смысла. Вы пытаетесь догнать наивную С++-версию. Сравнивайте с наивным rust-кодом. Вы же что делаете? Вы добавляете хаки, оптимизации и прочее, что не будет использоваться в обыденной жизни. В обыденной жизни будет использоваться базовый подход.

Далее, вы ускоряете версию в которой проблем меньше, чем у С++. Вы пытаетесь добавить zero-copy парсинг, убрать копирование, добавить более оптимальные хешмапы, cow и прочее. Но проблема никуда не делал — это сравнение глупое. Ведь всё это(и даже больше) есть в С++-коде и я там же могу этим заняться. И если вы даже наивную С++ до сих пор не догнали, то что будет в том случае? В случая, когда она будете не наивная.

Нужно всегда сравнивать что-то в равных условиях. Подобный вашему подход работает в ситуации сравнению с тем же jua. jua не может во всё это и на этом можно играть.

Я разбираюсь почему она отличается.

Ну нет же. В С++ есть zero-copy-парсинг? Нету. Там даже поточного парсинга нету. Задача «понять почему отличается» — это не пытаться найти другой подход и как-то догнать. Задача понять заключается в том, что-бы узнать. Почему в одном и том же подходе оно отличается. Почему С++ без всего этого работает быстро, причём работает быстрее даже самой оптимизированной вашей версии.

Эм, а что и многопоточная версия есть?

Да. Я специально после написания этой ветке пошел и сделал его.

У вас всё в порядке? Обычное желание выжать максимум из кода. Не надо придумывать какие-то странные мотивации.

Вы не выжимаете максимум — вы сравниваете. Сравниваете неадекватно. Сравниваете одно/много-поточные версии. Сравниваете наивные и не-наивные решения.

Выше вы писали, что хотите разобраться. Сейчас вы пишите, что вы просто пытаетесь догнать любыми методами. И в этом проблема. Все эти ухищрения, в том числи и многопоточность, можно добавить в С++. Это вам никак не поможет разобраться.

Это проблема вашего подхода. Обозначу проблему ещё раз. Нет смысла делать быстрее за счёт изменения подхода применяемого в С++. Вы этим ничего ничего не добиваетесь и причина простая. Всё это подходы можно точно так же добавить в С++ и они на него повлияют точно так же, даже больше.

+1
Сравниваете одно/много-поточные версии.

Производительность однопоточного С++ кода я привёл, чтобы была точка отсчета для связи с приведёнными результатами на других машинах. Многопоточного С++ кода тогда ещё по-моему не было.


Сравниваете наивные и не-наивные решения.

Rust ориентирован на безопасность кода. Наивное решение на Rust и наивное решение на C++ (которое ещё и работает) требуют разных уровней знания языка.


Кроме того, использование вами string_view и других последних фишек языка, как-то не выглядит очень наивным. Какой у вас опыт разработки на C++?


Нет смысла делать быстрее за счёт изменения подхода применяемого в С++.

Подход, применяемый в rapidjson, и так отличается от того, что применяется в serde. Например, SIMD intrinsic'и в serde пока ещё не применяются. Видимо это и является причиной, почему я с трудом догнал его по производительности на своей машине даже с zero-copy.


Стоп, стоп, стоп. Я посмотрел код rapidjson/document.h и ещё раз глянул ваш код.


std::string_view key{x.name.GetString(), x.name.GetStringLength()};

Пожалуйста, объясните, каким образом это не zero-copy? x.name.GetString() возвращает указатель на буфер, если есть такая возможность. Или я не прав?

0
Производительность однопоточного С++ кода я привёл, чтобы была точка отсчета для связи с приведёнными результатами на других машинах.

Нет, вы именно сравнивали. Как минимум автор статьи декларировал это сравнение явно, а вы просто сравнивали. Причём сравнивали непонятно где. И почему-то 200мс сустайма у вас вопросов не вызвало как и запуск не под линуксом.

Многопоточного С++ кода тогда ещё по-моему не было.

Я уже отвечал на этот вопрос. Дело в том, что добавление многопоточности — это не оптимизация в ситуации, когда и тот и тот язык умеет в многопоточность. Тут можно долго говорить о просьбе автора, но факт остаётся фактом. Это обстоятельство нужно уточнять и сравнивать несравнимое нельзя.

Rust ориентирован на безопасность когда. Наивное решение на Rust и наивное решение на C++ (которое ещё и работает) требуют разных уровней знания языка.

Насколько я вижу в интернете, везде вся эта безопасность декларируется как «zero-cost».

Кроме того, использование вами string_view и других последних фишек языка, как-то не выглядит очень наивным.

Я уже так же отвечал на этот вопрос. Единственное место, которое я паписал «не очень наивно» — это main::next. Именно там сосредоточено почти всё использование string_view.

Какой у вас опыт разработки на C++?

Смотря как считать. Если брать именно С++, то может года 2-3 суммарно.

Подход, применяемый в rapidjson, и так отличается от того, что применяется в serde.


Например, SIMD intrinsic'и в serde пока ещё не применяются.

Я вижу этот rapidjson первый(или второй) раз в жизни, как и саму задачу парсинга json«а. Полистав код я вижу только одно, что я уже и говорил. Этой библиотеке очень и очень далеко до оптимальной.

По поводу simd. Они там мало что дают. Да и сделано это крайне наивно.

Видимо это и является причиной, почему я с трудом догнал его по производительности на своей машине даже с zero-copy.

Нет, не является. Я проверил. Они там не включены по умолчанию, а если их включить, то они только замедляют код.

Пожалуйста, объясните, каким образом это не zero-copy? x.name.GetString() возвращает указатель на буфер, если есть такая возможность. Или я не прав?

Нет, это не zero-copy. В данном случае я использую sv как обёртку над „указатель + длинна“, которую мне отдают библиотка. Использую я это для красивого сравнения строк через ==, а не через memcmp(который за меня вызовет sv).

Все строки, которые выходят из это функции — это std::string, в который копируются данные. Везде и всюду, где использует sv — он используется как удобная обёртка над указателем + длинна. Никакой zero-cost функции он не несёт.

Причины я обозначил. Я писал аналогичный rust-коду автора код. Единственное место, где я отошёл от этого правила — это main::next и чтение файла. Причина банальна — так проще.

zero-cost есть в многопоточной реализации, именно так строки отправляют через очередь на парсинг.

0
Нет, не является. Я проверил.

Да, ошибся. SIMD включается флагами RAPIDJSON_SSE2 и RAPIDJSON_SSE42


Нет, это не zero-copy.

Да, разобрался. zero-copy используется только если использовать буфер — мутабельную строку. Если правильно понял код, то для аллокации распознанных значений используется CrtAllocator — как и в Rust без Jemalloc.


Извиняюсь. Невнимательно посмотрел код.


Почему Rust немного отстаёт даже в zero-copy режиме пока совершенно непонятно.

0
Производительность Rust кода теоретически не должна отличаться от C++

ключевое слово «теоретически». Даже если устранить все различия в реализациях одного и того же кода (для этого в Rust зачастую придется обмазаться unsafe с ног до головы, нивелируя все его достоинства), компилятор rust'а может использовать менее эффективную декларацию о вызовах, как, например, можно посмотреть здесь (пример позаимствован из доклада Антона Полухина antoshkka на с++russia 2019)
+1
Даже если устранить все различия в реализациях одного и того же кода (для этого в Rust зачастую придется обмазаться unsafe с ног до головы, нивелируя все его достоинства)

Аналогичная проблема возникла и тут. Даже наивный код в котором C++ и его stdlib очень просто победить. Не побеждается. И чем ниже уровень, тем больше проблем. Ведь rust задизайнен именно для наивногохайлевел программирования.

Да, можно говорить о том, что код на rust более безопасный. В С++ можно попробовать заменить list на vector. Но нельзя говорить о том, что эта безопасность достаётся бесплатно. Как и по части производительности, так и по части применяемых усилий. Очень много ложится на голову программиста.

Есть ещё один нюанс, о котором всё время забывают. Не бывает быстрых/медленных языков. Есть языки с большими/меньшими возможностями. Производительность/безопасность кода зависит исключительно от программиста. И именно на этом этапе проявляется различие языков. Какой-то язык одному уровню программистов даёт писать код быстрее/безопаснее, чем другим. Но для другого уровня — всё может быть наоборот.

Если мы начнём говорить про производительность, то тут прежде всего определяющим является умение программиста. А язык лишь средство выражения его умения/опыта. Язык должен дать возможность выразить программисту то, что он хочет и то как он это хочет. В этом основания проблема rust. В подобных случаях он не просто не помогает — он мешает. Причём порою мешает до невозможности.

Именно поэтому нет никакого смысла говорить о высокой(не в сравнении с питоном/swift, а объективно) производительности. Раст попросту не даст возможности программисту раскрыть свой потенциал. Именно поэтому уровень программистов будет относительно мал. Человек должен быть крайне фанатичен(в сторону rust) что-бы несмотря на все горечи и страдания пытаться писать на нём высокопроизводительный код.

компилятор rust'а может использовать менее эффективную декларацию о вызовах

Есть ещё одно обстоятельство касательно llvm. В нём вполне адекватные хайлевел-оптимизации. В ситуациях, когда дело касается числодробильного кода- он крайне слаб.

За примерами ходить ненужно. Тот же rapidjson собранный gcc в полтора раза быстрее собранного clang/llvm.
-3
И это zero-cost. Чтобы получить гарантированно валидный String из неконтролируемых внешних данных

Как строка связана с utf8? Как минимум существуют строки без юникода, а даже если с юникодом — существуют разные форматы юникода.

Это не базовая логика.

Это базовая логика. Почему именно она у вас медленнее неважно. Факт есть факт — она медленнее, со всеми вытекающими.

Это — 1) валидация utf-8, 2) моя ошибка с десериализацией Cow (значение всегда копировалось), 3) оверхед на работу с потоками.


1) уберите. 2) у меня в С++ коде всё копируется и нет проблем. 3) у меня никого оврехеда нет. Почему?

Заодно добавил кастомный аллокатор, чтобы немного отыграть проигрыш на флагах mmap.

Зачем вы добавили С++-зависимость к коду на rust? Ваши биндинги не могут во флаги?

На моей машине версия с аллокатором обгоняет ваш код на 70ms, без аллокатора на 50ms.

Нет. Я уже говорил, что у вас 180ms systime. Такого быть не должно.
+2
Как строка связана с utf8?

Это инвариант типов String и str — они содержат валидный utf-8, если не содержат — UB.


Нет. Я уже говорил, что у вас 180ms systime. Такого быть не должно.

Но есть. Платформа WSL.


$ uname -v
#379-Microsoft Wed Mar 06 19:16:00 PST 2019

Откуда столько systime пока не разобрался.


И, после общения в таком тоне, не очень хочется.

-3
Это инвариант типов String и str — они содержат валидный utf-8, если не содержат — UB.

Ещё раз. Это локальные проблемы отдельной библиотеки. Реальная жизнь на строки никакого подобного инварианта не накладывает. Строки это не только utf-8.

Но есть. Платформа WSL.

Это явно не обозначенная автором платформа.

И, после общения в таком тоне, не очень хочется.

Не хочется запускать код на той платформе, которую обозначил автор?

0

В Rust строки — это utf-8 строки (байтовый массив + проверка на utf-8-корректность последовательности байт). Если нужны какие-то иные строки, используется байтовый массив без проверок.

-4
В Rust строки — это utf-8 строки (байтовый массив + проверка на utf-8-корректность последовательности байт).

Мне какая разница с этого? Меня начали уверять, что строки такими «и должны быть» — я на это отвечал. Они такими быть не должны.

Если нужны какие-то иные строки, используется байтовый массив без проверок.

Мне никакой rust и никакие его строки использовать ненужно. Зачем вы мне это пишите? Вы это пишите тем, кто оправдывал этим малую производительность раста. Пусть они их используют и, возможно(крайне маловероятно) перестанут сливать lua.
0

Конечно, в строках должны быть символы, а не рандомный мусор. Если вы не валидируете строки "и так сойдет", то тут исключительно ваши проблемы.


Считать дефолтом объект, в котором возможно строка, а возможно и хрень видимо эффект проф деформации написания на С или С++.

-2
utf16 — это мусор, я правильно понял? Как и тысячи других кодировок.
+2

Пока вы не провалидировали набор байт — у вас набор байт, а не кодировка. Хоть UTF16 хоть любая другая. Даже то что это ASCII надо убедиться.

-5
Пока вы не провалидировали набор байт — у вас набор байт,

Полная чушь. Вы опять показываете свою полную несостоятельность. Никакой валидации utf16 не требует, я специально упомянул именно его, что-бы в очередной раз доказать ваш уровень понимания темы.

Ещё раз. Вы сказали, что всё что не utf8(всё во что не может ваша «строка») — мусор. Вернее мусор всё то, что не понимают строки в вашем супер-языке сливающему lua. И я даже не буду рассказывать, что никакой строке вообще никакая валидация/utf8 ненужны.

У вас два варианта. Либо продолжать отрицать очевидное(что строки и utf8 являются синонимом), либо признать ошибку.

У меня возникает такой вопрос, почему каждая ваша попытка со мною спорить — заканчивается полным вашим поражением?
+2
И я даже не буду рассказывать, что никакой строке вообще никакая валидация/utf8 ненужны.

image

-3
Есть что ответить? Или опять, типичная для хабра, клоунада? Доказывать необходимость/нужность — это ваша задача, а не моя. Вы это утверждаете, а не я. Доказывать ненужность чего угодно я не обязан — это состояние всего по умолчанию и это не требует доказательств по причине невозможности доказательства ненужности.
0
Полная чушь. Вы опять показываете свою полную несостоятельность. Никакой валидации utf16 не требует, я специально упомянул именно его, что-бы в очередной раз доказать ваш уровень понимания темы.

Вам по сети передают прям UTF16? Или может все же набор байт, который вы потом интерпретируете? Покажите исходый код библиотеки, которая вам UTF16 возвращает, и я вам покажу, как она производит конвертацию из байтового массива. В лучшем случае как и в расте, а в худшем как вы предлагаете, через статик_каст какой-нибудь.

-2
Ещё раз. Ваша задача — показать мне валидатор utf16. Вы утверждали обратное.

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

Повторю ещё раз. Валидатор utf16, а так же доказательства того, что «только валидированный utf8 является строкой». А так же, что меня вообще в какой-то мере должна волновать какая-то валидация, но это уже другая история и на неё можно не отвечать.
0

При чем тут валиадатор UTF8? Я сказал, что набор байт должен быть провалидирован. В расте дефолтная строка это utf8. Значит и в плюсовом коде должен быть utf8, иначе сравнение не до конца корректное. А уж сравнивать тупо с байтовым массивом тем более неправильно.

-2
При чем тут валиадатор UTF8?


Что я писал:

Как строка связана с utf8? Как минимум существуют строки без юникода, а даже если с юникодом — существуют разные форматы юникода.

Ещё раз. Это локальные проблемы отдельной библиотеки. Реальная жизнь на строки никакого подобного инварианта не накладывает. Строки это не только utf-8.


Вы пытались со мною спорить? Пытались. Что вы опровергали? Опровергали эти утверждения. Значит, вы утверждаете, что строки бывают только utf8 и что все строки должны его валидировтаь.

Конечно, в строках должны быть символы, а не рандомный мусор. Если вы не валидируете строки «и так сойдет», то тут исключительно ваши проблемы.

Вот тот набор слов, который я услышал на процитированные выше тезисы(мои). Я говорил о не-utf8 строках. Мне в ответ, среди этого набора слов, поступило заявление:

Конечно, в строках должны быть символы, а не рандомный мусор.

Т.е. под символами тут имеется ввиду utf8(хотя какое отношение utf8 и его валидация имеет к символам, но ладно), а «рандомным мусором» называется всё, что не является валидированным utf8.

В расте дефолтная строка это utf8. Значит и в плюсовом коде должен быть utf8, иначе сравнение не до конца корректное.

С чего вдруг? Эта проблема вашего «языка сливающего луа», а не мои. По этой «логике» у вас должен быть ГЦ, когда вы сравниваете что-то с луа(хотя это не помогло). Ведь тогда сравнение нечестное? Но это глупость.

Код должен выполнять поставленную задачу. Если мне нужна валидация utf8 и сам utf8 — она будет и должна быть. Если не нужна, то не будет.

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

Это вари проблемы, что ваш «язык» сливает и не даёт вам выбора. И это не значит, что этот выбор ненужен. Это значит, что выбора нет у вас. И на меня это никак распространятся не должно.
+2
Никакой валидации utf16 не требует

Насколько я знаю utf-16 поддерживает суррогатные пары (если лидирующее слово содержит 11011 в старших разрядах). При этом последующее слово обязательно должно содержать 110111 в старших разрядах. Если это не так, то последовательность невалидна.

-1
Насколько я знаю utf-16 поддерживает суррогатные пары

Действительно, судя по: www.unicode.org/faq/utf_bom.html#utf16-11 сейчас под utf16 понимается именно формат кодирования. Т.е. я был не совсем точен — пусть будет UCS-2/UCS-4/да пусть та же ascii.

При этом последующее слово обязательно должно содержать 110111 в старших разрядах. Если это не так, то последовательность невалидна.

Я не нашел подобных ограничений. Можете предоставить ссылку на то, что стандарт utf16 как-то ограничивает значение второго числа из пары.
+2

ASCII семибитная кодировка, так что если старший бит единица, то строка опять невалидна.

-2
Неверно. Там нет такого понятия как «невалидный». Опять глупости вызванные полным непониманием темы. ascii — это таблица, а как там они хранятся и в каком виде — абсолютно неважно. Тут идёт попросту путаница с utf8, где utf8 — это именно формат кодирования, а не таблица кодов.
-4
Нет, это так не работает. Вас поможет не это, а основания(верифицируемые) для признания байта «222» невалидным.

Хотя даже этого ненужно. Кто вас вообще сказал, что какой-то там байт вообще какой-то символ? Это, как я уже говорил, так не работает. Семибитную «кодировку» вообще не должно волновать то, что там лежит слева, сбоку и в каких-то левых битах.

Причина тому проста. Если у нас там 0, то чем это отличается от 1? Где именно ascii предписывает(даже в древнем rfc 0 там только рекомендация. К тому же это именно битовый формат, а не таблица. И тут, опять же, ненужно путать), что в каких-то других битах должен быть ноль? Почему не 1?

Именно поэтому подобные рассуждения опять сводятся к непониманию темы. Какая-то кодировка является лишь интерпретацией каких-то битов. Никаких интерпретаций других битов — она не даёт. Поэтому она могут быть какими угодно. В этом её свойство.

utf8(и тот же 16) — это не таблица, а формат кодирования. Именно в нём формализовано то, что является ошибкой, а что не является. Прям на уровне битов и прям всех байтов.

Таблица же определяет лишь соответствия уровня число->символ. Она нигде не определяет форматы чисел, какие и где там могут быть записаны числа. Сколько их должно быть и какой они должны быть ширины.

И самое главное, и об этом я уже говорил. Никто и нигде не накладывает ограничений на канальный уровень. Никто и нигде не накладывает ограничений на вход в декодер. Именно поэтому такого понятия как «валидация» вообще существовать не может — это глупость. Валидация является лишь частью декодера и не более того.

С т.з. всего остального — никакой валидации в принципе существовать не может, да и она не имеет смысла.

0

В UCS-2/UCS-4, опять-таки, существуют невалидные коды. Из-за тех самых суррогатных пар. Собственно, суррогаты как раз и невалидны, поскольку соответствующих им символов в Юникоде не существует.

-2
В UCS-2/UCS-4, опять-таки, существуют невалидные коды.

Там не может подобного существовать в принципе. Это просто таблица.

Из-за тех самых суррогатных пар. Собственно, суррогаты как раз и невалидны, поскольку соответствующих им символов в Юникоде не существует.

Не существует — не значит невалидно.

В целом я ваши рассуждения вообще не понимаю. Вы откуда-то взяли какое-то «валидно», но ссылаетесь на декодирование(которое вы, очевидно, не делаете). Вы каким-то образом накладываете критерий валидности на передающий канал, хотя никакого подобного упоминания нет. И это попросту бред. «Валидация» — это задача декодера.

В конечном итоге была придумана какая-то глупость и путём подмены понятий с кого-то начинается требование. Я вообще могу сослаться на декодер и то, что декодер является валидатором, а значит МОЖЕТ принимать невалидную последовательность.

Из этого напрямую следует то, что на входящую последовательность НИКАКИХ ограничений нет. А значит никакого смысла валидации нет и вообще её нет. Это ваша локальная придумка.
0
Можете предоставить ссылку на то, что стандарт utf16 как-то ограничивает значение второго числа из пары.

10 разряд обоих слов пары используется для указания признака лидирующего или последующего слова. А пять старших разрядов у обоих слов должны содержать 11011. Так что для кодирования символа в паре используется только 20 разрядов:


U' = yyyyyyyyyyxxxxxxxxxx
W1 = 110110yyyyyyyyyy
W2 = 110111xxxxxxxxxx

UTF-16, an encoding of ISO 10646 — 2.1 Encoding UTF-16

-3
If there is no W2 (that is, the sequence ends with W1), or if W2
is not between 0xDC00 and 0xDFFF, the sequence is in error.
Terminate.

Т.е. там попросту есть ограничения на значения во второй части пары.

При этом последующее слово обязательно должно содержать 110111 в старших разрядах. Если это не так, то последовательность невалидна.

Этого я там не нашел.

W1 is between 0xD800 and 0xDBFF
W2 is between 0xDC00 and 0xDFFF

Это все ограничения, которые я там увидел.

Но в любом случае — ограничения на значения в utf16 есть. Значит она может быть невалидной.

Also note that a string
decoding algorithm, as opposed to the single-character decoding
described above, need not terminate upon detection of an error, if
proper error reporting and/or recovery is provided.

Хотя тут допускается игнорирование ошибок. Зарепортить можно когда и как угодно. Аналогично с восстановлением. После, по-факту использования я могу попросту игнорировать «битые» пары. Насколько я понимаю под восстановлением тут понимается «восстановление владиности последовательности».

+1
If there is no W2 (that is, the sequence ends with W1), or if W2 is not between 0xDC00 and 0xDFFF, the sequence is in error.
Terminate.

Т.е. там попросту есть ограничения на значения во второй части пары.


При этом последующее слово обязательно должно содержать 110111 в старших разрядах. Если это не так, то последовательность невалидна.

Этого я там не нашел.



0xDC00 = 1101110000000000
0xDFFF = 1101111111111111

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

Как насчёт строки состоящей из двух байт 0x00, 0xd8 в utf-16 le. Всё ещё не требует валидации? Или разницы между UTF-16 и UCS-2 по-вашему нет?

-4
Всё ещё не требует валидации?

Я уже отвечал на это и на тысячи подобных вопросов. Я до сих пор не понял зачем вы говорите о какой-то валидации. Зачем она нужна? Какой в ней смысл? Делать два раза одну и туже работу?

Есть ли в utf16 «неправильные» последовательности, либо нет — так же неважно. utf16 лишь пример, которую я перепутал с ucs-2, а даже если бы не перепутал — это так же ничего не меняет.

Факт остаётся фактом — строка является транспортом для данных. Никакой транспорт валидировать ненужно и это не имеет смысла.

Аналогично и с utf8. Строка не с utf8 не становится мусором, как утверждалось выше. И даже если мы примем какую-то необходимость валидации строки, то не все форматы кодирования избыточны.
+2

Спасибо за ваше квалифицированное мнение. Некоторые писатели парсеров ASN.1 тоже заявляли подобное, потом можно было спокойно выпускать сертификат на "*.google.com\0.my-evil-domain.ru" и браузеры его прекрасно интерпретировали как google.com. И куча других не менее прелестных CVE где причиной стало то, что разработчики не обращают внимания на то что в разных контекстах строка может быть не просто char * или uint8_t * с null-termination, но и чем-то другим. Другой хороший пример, что "транспорт валидировать не надо" назывался heartbleed. Помните такой?


Если так хочется сравнивать использовать в rust'е, например, "строки" не с utf8 никто не мешает использовать срезы байт (&[u8]), которые эффективно являются указателем + длинной как и плюсовый string_view или какую-нибудь OsString, который имеет более слабые инварианты на содержимое нижележащего массива байт.

-4
Спасибо за ваше квалифицированное мнение.

Ну кому-то же надо его иметь.

Некоторые писатели парсеров ASN.1 тоже заявляли подобное, потом можно было спокойно выпускать сертификат на "*.google.com\0.my-evil-domain.ru" и браузеры его прекрасно интерпретировали как google.com.

Какая-то чушь. Оно так интерпретируется именно потому, что интерпретируется «правильно». А вот если бы это был «массив байт», то никогда такого бы не произошло. Это очевидно.

куча других не менее прелестных CVE где причиной стало то, что разработчики не обращают внимания на то что в разных контекстах строка может быть не просто char * или uint8_t * с null-termination, но и чем-то другим.

Больше базвордов, больше отсылок на фентейзиные cve.

Ваши рассуждений сломались так и не начавшись. Изначально речь шла о «всё что не utf8 — мусор». Теперь вдруг риторика поменялась и началось какое-то char, null и прочие не относящиеся к теме рассуждения.

Другой хороший пример, что «транспорт валидировать не надо» назывался heartbleed. Помните такой?

Такая же глупость. heartbleed вообще никак не связан с темой. Это глупая попытка выдернуть фразу, интерпретировать как угодно. Очевидно, что под транспортом имелось ввиду «транспортируемые данные».
Если так хочется сравнивать использовать в rust'е, например, «строки» не с utf8

И опять враньё. Строк не с utf8 нету. Это всё мусор. Зачем мне использовать мусор?

К тому же, вы сейчас умножили на ноль все предыдущие рассуждений. Если это делать можно, то зачем все эти рассуждения про cve/hb? Пустить пыль в глаза?

Все эти рассуждения имеют единственную предпосылку «у вас можно сделать неправильно». Если неправильно можно сделать везде, то смысла в этих рассуждениях нет.

никто не мешает использовать срезы байт (&[u8]), которые эффективно являются указателем + длинной как и плюсовый string_view или какую-нибудь OsString, который имеет более слабые инварианты на содержимое нижележащего массива байт.

К эффективности rust не имеет никакого отношения. По поводу «можно» — это такая же глупость.

Меня не должно волновать слово «можно», меня должно волновать слово «как». Можно и черепом гвозди забивать, но я так делать не буду.

Определяющим является не то, что там можно через какое-то одно место(и то, можно только номинально). А то, насколько всё это прозрачно и удобно происходит.

0

Во-первых, у вас детектор сарказма сломался.


Оно так интерпретируется именно потому, что интерпретируется «правильно». А вот если бы это был «массив байт», то никогда такого бы не произошло. Это очевидно.

Ну-ну, т. е. паскалёвых и подобных строк не существует. И понятие строки из различных представлений ASN.1.


У меня сложилось впечатление, что Вы не осознаёте, что в различных контекстах понятие строки может быть различное. Где-то это просто byte array + lenght, где-то с требованием валидного UTF-16 в native endianness, где-то null terminated byte array, где-то byte array с указателем на объект encoder'а и decoder'а. И всё это строки.


Сравнивать стоит всё-таки сопоставимые вещи, о чём Вам здесь не раз сказали. Сравнивать реализацию с валидацией входного потока и без, да потом заявлять что реализация без валидации выше/лучше/быстрее выглядит как какой-то маразм. И цепляться после этого к тому что в другом языке и/или стандартной библиотеке под строкой могут понимать что-то другое — тоже.

-3
Ну-ну, т. е. паскалёвых и подобных строк не существует. И понятие строки из различных представлений ASN.1.

Попытка подменять понятия. Я говорил именно про данные, к паскаль/си-строке это не имеет никакого отношения.

Я уж не стал цепляться к ваши примерам с hb. Но я вам подскажу. Именно то, что openssl следовал вашей логике и получился hb. Именно потому, что openssl пытался не отделял транспорт от транспортируемого и вышла подобная ситуация. Как и ваш другой пример был глуп.

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

Строка, как транспорт, с ними не работает и её абсолютно не должно волновать какие в ней лежат данные. Если вы хотите валидировать данные — их нужно валидировать на входе. Строка этим входом не является.

У меня сложилось впечатление, что Вы не осознаёте, что в различных контекстах понятие строки может быть различное.

Вы это не мне пишите, а тем, кто утверждал «строки — это наши строки с utf8, а всё остальное мусор». Для меня строка — некий транспорт для данных. Транспорту абсолютно без разницы что там внутри него.

Где-то это просто byte array + lenght

Это описание транспорта.

где-то с требованием валидного UTF-16 в native endianness, где-то null terminated byte array, где-то byte array с указателем на объект encoder'а и decoder'а. И всё это строки.

Вы должны спорить не со мною. С чем именно вы спорите? С моим тезисом «транспорт не должно интересовать транспортируемое»? Я не считаю обратное осмысленным. Вы можете с этим поспорить.

Ссылки на «я считаю, что можно и валидировать» не являются аргументом. Вы должны объяснить «зачем». Зачем делать что-то, что предлагаете делать.

Сравнивать стоит всё-таки сопоставимые вещи, о чём Вам здесь не раз сказали.

Я что-то не вижу вашей критики под «я сравниваю многопоточное/однопоточное», «я сравниваю sax/dom»? Зачем вы это пишите мне?

Кто вам не даёт убрать ненужную здесь валидацию? Это не моя проблема, что вы её не убрали. И это не было темой обсуждения. Вы опять врёте.

Темой обсуждение было «мы не убрали потому что убирать неправильно» — ваша задача обосновать почему. Зачем вы пытаетесь подменить контекст?

Сравнивать реализацию с валидацией входного потока и без,

Во-первых вы опять врёте, т.к. валидация в моём коде есть. Во-вторых, как я уже говорил, наличие валидации у вас — ваша проблемы. Она не ставилась как требование автора, а значит она ненужна. Вы должны обосновать её наличие. Если вы пытаетесь утверждать, что моя реализация неверня или что-то тому подобное.

К тому же, вас никак это не спасёт. Даже если я буду строку хоть 10 раз валидировать.

да потом заявлять что реализация без валидации выше/лучше/быстрее выглядит как какой-то маразм.

Опять враньё про отсутствие валидации. Опять враньё про причину проигрыша. Причина не в валидации. Повторюсь, кто вам мешал её убрать? Она была в требованиях автора? Нет. У вас она есть? Да. У вас с нею проблемы? Уберите её.

И цепляться после этого к тому что в другом языке и/или стандартной библиотеке под строкой могут понимать что-то другое — тоже.

Опять враньё. Почему каждый ваш тезис — враньё? Цеплялись вы. Вы начали оправдываться, вы начали утверждать, что строки только utf8 и всё остальное мусор.

С чего вдруг я должен идти на поводу у ограничения какого-либо пхп и заниматься какой-то ненужностью? Это ваша проблема, а не моя.

К тому же, где критика моих оппонентов которые действительно сравнивали несравнимой? Какая-то у вас однобокая справедливость получается. Почему та происходит?

После того, каким «сравнением несравнимого» поклонники очередного пхп пытались выиграть мой код — я могу говорить что угодно. Я не собираюсь играть по правилам, которые нарушаете вы. Вы их нарушили множество раз. После этого все обвинения меня не имеют смысла.

К тому же даже ваши обвинения ложны.

0
Я ошибся в merge_result, там нужно итеративно мерджить, ведь есть связи через несколько компаний, поэтому и результаты разные — в зависимости как данные между потоками распределились.
0
Я тоже ошибся в своей попытке локализовать проблемы. Мне почему-то показалось, что эти вектора статической длинны, но судя по всему это что-то аналогичное sso.

Как только у меня появится время займусь оптимизацией С++-варианта.
0
merge я реализовал простым перебором. Не знаю правильно это или нет.

Только сейчас увидел:
$ time ../habr2_cpp/a.out 1.json
[...]
real 0m0.653s
user 0m0.453s
sys 0m0.188s

Судя по sys — с системой явно что-то не то.

Многопоточная С++-версия
//https://github.com/jarro2783/cxxopts.git
//https://github.com/Tencent/rapidjson.git
//https://github.com/cameron314/concurrentqueue.git
#include <rapidjson/document.h>
#include <filesystem>
#include <sys/mman.h>
#include <fcntl.h>
#include <unordered_map>
#include <charconv>
#include <string>
#include <unordered_set>
#include <list>
#include <array>
#include <vector>
#include <boost/algorithm/string/join.hpp>
#include <algorithm>
#include <cxxopts.hpp>
#include <concurrentqueue/concurrentqueue.h>
#include <future>

struct rec_t {
  std::string company;
  std::vector<std::string> phones;
  double debt;
};

struct debtor_t {
  std::unordered_set<std::string> companies;
  std::unordered_set<std::string> phones;
  double debt;
};


struct debtors_t {
  std::unordered_map<std::string, debtor_t &> by_phone_index;
  std::list<debtor_t> all;
};

void process_object(debtors_t & debtors, rec_t & rec) {
  
  auto & index = debtors.by_phone_index;
  auto op = [&](debtor_t & d) {
    d.companies.insert(std::move(rec.company));
    for(const auto & phone: rec.phones) {
      d.phones.insert(phone);
//       if(!index.contains(phone))
      if(index.find(phone) == end(index))
        index.emplace(phone, d);
    }
    d.debt += rec.debt;
  };
  
  auto di = end(index);
  for(const auto & phone: rec.phones) {
    di = index.find(phone);
    if(di != end(index)) break;    
  }
  op((di != end(index)) ? di->second : debtors.all.emplace_back());
}


rec_t & extract_data(rapidjson::Document & doc, rec_t & rec) {
  rec.phones.clear();
  rec.company.clear();
  rec.debt = 0;
  
  for(auto & x: doc.GetObject()) {
    std::string_view key{x.name.GetString(), x.name.GetStringLength()};
    auto & value = x.value;
    
    auto val2str = [](auto & value) -> std::string {
      std::array<char, 32> mem;  
      
      switch(value.GetType()) {
        case rapidjson::kStringType: return {value.GetString(), value.GetStringLength()};
        case rapidjson::kNumberType: {
          auto [endp, _] = std::to_chars(begin(mem), end(mem), value.GetUint64());
          return {begin(mem), endp};
        }
        default: return {};
      }
    };
    
    auto val2num = [](auto & value) -> double {
      switch(value.GetType()) {
        case rapidjson::kStringType: return strtod(value.GetString(), nullptr);
        case rapidjson::kNumberType: return value.GetDouble();
        default: return {};
      }
    };
    
    auto push_phone = [&](auto & value) {
      if(auto phone = val2str(value);!phone.empty())
        rec.phones.emplace_back(phone);
    };
    
    if(key == "company") {
      if(value.IsString()) {
        rec.company = {value.GetString(), value.GetStringLength()};
      } else if(value.IsObject()) {
        auto name = value.GetObject().FindMember("name");
        if(name != value.GetObject().MemberEnd()) rec.company = val2str(name->value);
      }
    } else if(key == "phone") {
      push_phone(value);
    } else if(key == "phones") {
      if(value.IsArray()) for(auto & x: value.GetArray()) push_phone(x);
      push_phone(value);
    } else if(key == "debt") rec.debt = val2num(value);
  }
  
  return rec;  
}

void print(const std::list<debtor_t> & all) {
  for(size_t it = 0; auto & d: all) {
    printf("-------------------------------\n");
    printf("#%lu: debt: %f\n", it++, d.debt);
    using boost::algorithm::join;
    printf("companies: {%s}\nphones: {%s}\n", join(d.companies, ",").c_str(), join(d.phones, ",").c_str());
  }
}

auto merge(std::vector<std::list<debtor_t>> & results) {
  std::list<debtor_t> result, tmp;
  for(auto & x: results) result.splice(end(result), x);
  if(result.empty()) return result;
  for(size_t c = 1; c;) {
    tmp.splice(std::begin(tmp), result, begin(result), ++begin(result));
    auto & a = tmp.front();
    c = 0;
    re:
    for(auto & phone: a.phones) {
      for(auto it = begin(result); it != end(result); ++it) {
        if(auto & b = *it; b.phones.find(phone) != end(b.phones)) {
          a.companies.merge(b.companies);
          a.phones.merge(b.phones);
          a.debt += b.debt;          
          result.erase(it);
          ++c;
          goto re;
        }
      }      
    }
    result.splice(end(result), tmp);
  }
  assert(tmp.empty());
  return result;  
}



moodycamel::ConcurrentQueue<std::string_view> q;

struct config {
  static inline size_t bulk = 128;
  static inline size_t concurrency = std::min(4u, std::thread::hardware_concurrency());
};

auto work() {
  debtors_t debtors;
  std::vector<std::string_view> pool(config::bulk);
  rec_t rec;
  
  for(bool end_flag = false; !end_flag;) {
    auto n = q.try_dequeue_bulk(begin(pool), config::bulk);
    rapidjson::Document doc;
    for(size_t it = 0; it < n; ++it) {
      auto & str = pool[it];
      if((end_flag = str.empty())) continue;
      doc.Parse(data(str), size(str));
      process_object(debtors, extract_data(doc, rec));      
    }
    
    if(end_flag) q.enqueue({});
  }
  return debtors.all;
}


auto parse(int argc, char* argv[]) {
  cxxopts::Options options{*argv};
  options.add_options()
    ("b", "", cxxopts::value(config::bulk))
    ("c", "threads", cxxopts::value(config::concurrency))
    ("f", "file path", cxxopts::value<std::string>());
  auto pr = options.parse(argc, argv);
  if(pr.count("f") == 1) return pr["f"].as<std::string>();
  printf("%s\n", options.help().c_str());
  exit(0);
}


int main(int argc, char * argv[]) {
  std::filesystem::path path = parse(argc, argv);
  auto file_size = std::filesystem::file_size(path);
  auto file_data = (const char *)mmap(nullptr, file_size + 4096, PROT_READ, MAP_POPULATE|MAP_PRIVATE|MAP_FILE, open(path.c_str(), O_RDONLY), 0);
  std::string_view file{file_data, file_size};
    
  auto next = [it = file]() mutable {
    auto p = it.data();
    const char * begin = nullptr;
    size_t braces = 0;
    while(true) {
      switch(p = strpbrk(p, "{}"); p ? *p : 0) {
        case '{': if(!braces++) begin = p; ++p; break;
        case '}': ++p; if(!--braces) {
          it = it.substr(size_t(p - it.begin()));
          return std::string_view{begin, size_t(p - begin)};
        } break;
        default: it = it.substr(it.size()); return std::string_view{};
      }
    };
  };
  
  std::vector<std::future<std::list<debtor_t>>> tp(config::concurrency);
  for(auto & f: tp) f = std::async(std::launch::async, work);
  
  std::vector<std::string_view> pool;
  auto max = config::bulk * config::concurrency;
  pool.reserve(max);
  while(true) {
    auto str = next();
    if(pool.size() == max || str.empty()) {
      q.enqueue_bulk(begin(pool), pool.size());
      pool.clear();
    }
    if(str.empty()) {
      q.enqueue({});
      break;
    }
    pool.emplace_back(str);    
  }
  std::vector<std::list<debtor_t>> all;
  std::transform(begin(tp), end(tp), std::back_inserter(all), [](auto & f) {
    return f.get();
  });
  assert(all.size() == config::concurrency);
  print(merge(all));
}



Результат был понятен изначально:

//C++ 4 потока
real 0m0,137s
user 0m0,557s
sys 0m0,012s

//C++ 1 поток + taskset на 1 ядро
real 0m0,526s
user 0m0,510s
sys 0m0,016s

//далее 400 мегабайт json
//C++ 4 потока + taskset на 4 ядра
real 0m0,547s
user 0m2,057s
sys 0m0,042s

//C++ 1 поток + taskset на 1 ядро
real 0m2,074s
user 0m2,034s
sys 0m0,040s

//rust + taskset на 4 ядра
real 0m0,956s
user 0m3,696s
sys 0m0,070s



Правильный рост, никакого оверхеда, никакого изменения кода. Написано максимально наивно.
+1
Я тут многопоточку написал, на 4-х ядрах она точно разделает сишку,

Нет, но близко. ~0.7 против ~0.55. Нужно больше потоков.
0
а сколько у вас потоков, там же параметр есть, и дополнительно вид обмена — синхронный медленнее но память не жрет.
0
Я проверял на 4х потоках. На том и том параметре. Разницы между параметрами у меня минимальна.
0

Алгоритм не правильный.


Пример

[
{"company":"Рога и копыта", "debt": 800, "phones": [123, 234, 456]},
{"company":"Первая коллекторская", "debt": 1200, "phones": ["2128506", 456, 789]},
{"company":"Святой престол", "debt": "666", "phones": 666},
{"company": "Казачий спас", "debt": 1500, "phones": [234567, "34567", 789], "phone": 666},
{"company": {"name": "Шестерочка"}, "debt": 2550, "phones": 788, "phone": 789}
]


Должно объеденить всё в одного, но это не происходит. Я думаю этот неправильный алгоритм перекочивал во все примеры из комментариев, кроме многопоточки на С++ там подругому merge сделан.

0
Мой косяк, надо итеративно мерджить, уже лень переделывать…
0

Тогда ТЗ не понятно, например HashSet для телефонов не нужен, достаточно общего HashMap. С учётом того что у нас там постоянно to_owned, выкидывание лишней строчки даёт ускорение на 10%. Например в последней версии на го вообще выкинули HashSet и всё в vector, что для десятка телефонов работает наверное быстрее с учетом константы. Но если телефонов тысяча тогда я уверен решение на го сольётся.

+1

Написал потоковый (как на Гошке) парсер на расте, с аллокациями не заморачивался, думаю можно ещё ускорить. Результаты на моём i5, это однопоточка:


  • rust = 636ms
  • gcc = 653ms
  • go = 663ms
  • clang = 799ms

Бойлерплейта много слабонервным не смотреть: https://gist.github.com/technic/5f23603ac955d246a90a8421a667c8c2#file-main-rs (лицензия кода на rust MIT)


PS На гист так же залил исходники плюсов и гошки с которой сравнивал. Надеюсь авторы не против.


PPS Возможно это не самая быстрые версии на альтернативных языках, что удалось найти среди комментов.

+1

Кстати автор ещё не оценивал решения на предмет обработки ошибок. Добавил мусор в джсон:


  • C++: Assertion `IsObject()' failed. Aborted (core dumped)
  • Go: Вывело не правильный результат ничего не сказав
  • Rust: Error("expected value", line: 1, column: 69)

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

0
Пишете Вы круто, но результат сравнения совершенно ужасен — платформозависимый модуль, прямой доступ к памяти, unsafe — тогда нафига нам нужен такой Rust, если гошка в совершенно безопасном и тупом как валенок коде практически ничем не хуже. Может поэтому на этом расте в России никто и не пишет?
+1

unsafe используется:


1) я написал очень упрощённый вариант glibc функции strpbrk, которая векторизирует поиск скобочек в строке (после беседы с twinklede). Без этого будет по-моему 700ms (там можно раскоментировать вариант с enumerate). Тогда даже на такой простой задаче С++ не догнать по определению, если прочий код будет работать одинаково. (Хотя я подозреваю что потоковый парсер плюсов мы не догоним). Без unsafe вы не получите sse42.


2) Прямой доступ к памяти это Вы про mmap? Возможно он не обязателен. Кстати mmap там кросс платформенный используется. Но во многих примерах выше он тоже присутсвовал.


Да я не аккуратно писал с unsafe, надо было как то завернуть в модуль, обложить комментариями и т п. По сути инвариант там всего один (я надеюсь :), нам надо скармливать буфер определённого размера в векторный регистр. Если дадим указатель на меньше памяти чем надо, то привет UB, я использовал растовый итератор chunks_exact а не арифметику с указателями чтобы сделать это более явно.


Алгоритм мержа у меня тоже не правильный. Я думал над тем какой лучше, но походу зависит от характера входных данных. Для нашего датасета, логичнее всего по быстрому отбрасывать повторения. Поэтому думаю это не так важно для сравнения. Upd: Вообще я подумал задача довольно странная мы соревнуемся в том чтобы как можно быстрее отбрасывать данные. Какая-то ддос атака. Достаточно было бы сравнить скорость сериализации до получения DebtRec.


Решение на го действительно выглядит красиво, просто и работает быстро. На serde писать свои Visitor далеко не так приятно. Но если данные не "грязные" а следуют обычному формату то можно было бы обойтись #[derive]. Хотя в го коде есть всякие читики, типа резервация векоторов на 10 елементов и вектора вместо set, я не знаю на сколько это влияет.


Я к сожелению на go не смог построить heatmap через perf, т.е. я построил но имена функции все unknown. Кто подскажет?

Only those users with full accounts are able to leave comments. , please.