Ускоряем Node.js с помощью Rust



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

    И так, делал я вот что. Есть проект на node.js. Там есть функционал, который требует считать хэш. При том, довольно часто — почти на каждый входящий запрос. Поскольку хэш этот не является чем-то, что должно уберечь меня от коллизий и вообще нужен не безопасности ради, а удобства для, то используется алгоритм adler32. Он предоставляет короткое выходное значение.

    По какой-то нелепости, в node.js его нет. Поясню, почему это нелепо. Этот алгоритм обычно используется в компрессии, в частности его использует gzip. В node.js есть стандартная реализация gzip в модуле zlib. То есть, adler32 там вообще-то есть, но в неявном виде. В Python, для сравнения, в аналогичном модуле он имеется и им можно пользоваться.

    Ну, да ладно. Берём сторонний пакет из npm. Я взял вот этот: adler32 — в основном потому, что он умеет интегрироваться с модулем crypto и его можно использовать так же, как и остальные хэш-алгоритмы. Это удобно. О производительности в данном случае я особенно не задумывался. Какой бы она ни была — это копейки. Но поскольку у меня намечался эксперимент, то этот самый adler32 был выбран жертвой.

    В общем, приступим. Ставится Rust просто. Документация тоже достаточно внятная как на русском, так и на английском. Rust взят версии 1.15. Забавный факт: документация на русском не является прямым переводом английской и немного отличается по структуре. В частности, в неё добавлен пример работы с потоками.

    Кроме самого Rust, стоит так же node.js версии 6.8.0, Visual Studio 2015 и Python 2.7 — это всё понадобится.

    Теперь проведём предварительный замер.

    Node.js


    for (var i=0; i<5000000; i++) {
        var m = crypto.createHash('adler32');
        m.update("Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму");
        m.digest('hex');
    }

    Средний результат трёх запусков: 41,601 секунда. Лучший результат: 40,206

    Чтобы с чем-то сравнить, давайте возьмём для начала нативную реализацию хэша в node.js. Скажем, sha1. Выполнив точно тот же самый код, но указав в качестве алгоритма sha1, я получил такие цифры:
    Средний результат трёх запусков: 9,737 секунд. Лучший результат: 9,321

    Может ну его вообще этот адлер? Но погодите, погодите. Давайте всё-таки попробуем что-нибудь сделать на Rust.

    Rust


    И так, на Rust есть сторонняя библиотека compress, которая доступна в этом их Cargo. Она тоже умеет gzip и предоставляет возможность считать adler32. Примерно так это выглядит:

    for i in 0..5000_000 {
        let mut state = adler::State32::new();
        state.feed("Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму".as_bytes())
    }

    Средний результат трёх запусков: 2,314 секунды. Лучший результат: 2,309

    Неплохо!

    Node.js и FFI


    Поскольку Rust компилируется в код, совместимый с Си, то его можно скомпилировать в динамическую библиотеку и подключать с помощью FFI. У node.js для этого есть специальный пакет, который нужно ставить отдельно:

    npm install ffi

    Если у вас всё хорошо, то после этого можно будет подключать внешние библиотеки написанные на Си или совместимые с ним.

    Значит, надо эту дробилку на расте преобразовать теперь в библиотеку. Если коротко, то код выглядит примерно так:
    extern crate compress;
    extern crate libc;
    
    use libc::c_char;
    use std::ffi::CStr;
    use std::ffi::CString;
    use compress::checksum::adler;
    
    #[no_mangle]
    pub extern "C" fn adler(url: *const c_char) -> *mut c_char {
        let c_str = unsafe {
            CStr::from_ptr(url).to_bytes()
        };
        let mut state = adler::State32::new();
        state.feed(c_str);
        let s:String = format!("{:x}", state.result());
        let s = CString::new(s).unwrap();
        s.into_raw()
    }

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

    Кроме того, в файле Cargo.toml нужно указать, что компилировать нужно в динамическую библиотеку. Там же указываются зависимости:

    [package]
    name = "adler"
    version = "0.1.0"
    authors = ["juralis"]
    
    [lib]
    name = "adler"
    crate-type = ["dylib"]
    
    [dependencies]
    compress = "*"
    libc = "*"


    Вот. Теперь это будет компилироваться в библиотеку. Какого типа — зависит от целевой платформы. У меня на выходе получилась dll, поскольку занимался я всем этим из под Windows и указал соответствующие параметры компиляции:

    cargo build --release --target x86_64-pc-windows-msvc

    Ну что ж. Хватаем эту самую dll, кладём куда-нибудь ближе к проекту на node.js и кое-что добавляем в код:

    var ffi = require('ffi');
    var lib = ffi.Library('adler.dll', {
        adler: ['string', ['string']]
    })
    for (var i=0; i<5000000; i++) {
        lib.adler("Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму")
    }
    

    Средний результат трёх запусков: 27,882 секунд. Лучший результат: 26,642

    Ну… Что-то как-то не то, что хотелось бы. Видимо, все эти радости с внешними вызовами стоят довольно дорого. Тем не менее, это всё-таки работает быстрее. Но можно ли сделать ещё быстрее? Можно.

    Node.js и С++ аддон


    В node.js, как известно, поддерживаются так называемые аддоны. Почему бы и не попробовать? Единственная проблема, что я вообще говоря в С++ ни в зуб ногой. Впрочем, есть добрые люди, которые написали немного справки. Вот тут примерно рассказано о том, как оно работает. Как оказалось, я не первый, кто решил таким образом поразвлечься. Впрочем, там довольно тривиальный пример с вычислением чисел Фибоначчи и соответственно, там многое остаётся неясным. А поскольку C++ я не знаю, то это конечно представляло проблему.

    Но оказалось, что человечество пошло гораздо дальше в вопросе придумывания всевозможных извращений и некий добрый человек написал небольшой генератор Cpp-обёрток для Rust-библиотек. Он анализирует исходники на Rust, берёт те функции, которые подходят по критериям и формирует какой-то код на плюсах. И вот для того Rust-кода, который был приведён выше, получился такой вот кусок кода на C++

    //Header
    //This could go into separate header file defining interface:
    #ifndef NATIVE_EXTENSION_GRAB_H
    #define NATIVE_EXTENSION_GRAB_H
    
    #include <nan.h>
    #include <string>
    #include <iostream>
    #include <node.h>
    #include <stdio.h>
    
    
    using namespace std;
    using namespace v8;
    using v8::Function;
    using v8::Local;
    using v8::Number;
    using v8::Value;
    using Nan::AsyncQueueWorker;
    using Nan::AsyncWorker;
    using Nan::Callback;
    using Nan::New;
    using Nan::Null;
    using Nan::To;
    
    #endif
    
    
    /* extern interface for Rust functions */
    extern "C" {
      extern "C" char * adler(char * url);
    }
    
     
    NAN_METHOD(adler) {
      Nan::HandleScope scope;
      String::Utf8Value cmd_url(info[0]);
      string s_url = string(*cmd_url);
      char *url = (char*) malloc (s_url.length() + 1);
      strcpy(url, s_url.c_str());
      char * result = adler(url);
      info.GetReturnValue().Set(Nan::New<String>(result).ToLocalChecked());
      free(result);
      free(url);
    }
    
    NAN_MODULE_INIT(InitAll) {
      Nan::Set(
        target, New("adler").ToLocalChecked(),
        Nan::GetFunction(New<FunctionTemplate>(adler)).ToLocalChecked()
      );
    }
    
    NODE_MODULE(addon, InitAll)

    Кроме того, у предыдущего товарища я взял пример файла bindings.gyp:

    {
        "targets": [{
            "target_name": "adler",
            "sources": ["adler.cc" ],
            "libraries": [
                "/path/to/lib/adler.dll"
            ]
        }]
    }

    Ещё нужен файл index.js с одержанием:

    module.exports = require('./build/Release/addon');

    Теперь надо всю эту радость собрать с помощью node-gyp. Но у меня оно компилироваться с наскока отказалось. Пришлось немного поразбираться в том, что там происходит.

    Для начала надо поставить пакет nan (Native Abstractions for Node.js ):
    npm install nan -g

    И добавить путь до него в bindings.gyp (где-нибудь на одном уровне с libraries):

    
            "include_dirs" : [
                "<!(node -e \"require('nan')\")"
            ]

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

    #include <nan.h>
    #include <string>
    #include <node.h>
    
    #pragma comment(lib,"Ws2_32.lib")
    #pragma comment(lib,"userenv.lib")
    using std::string;
    using v8::String;
    using Nan::New;
    
    extern "C" {
      extern "C" char * adler(char * url);
    }
    
    NAN_METHOD(adler) {
      Nan::HandleScope scope;
      String::Utf8Value cmd_url(info[0]);
      string s_url = string(*cmd_url);
      char *url = (char*) malloc (s_url.length() + 1);
      strcpy(url, s_url.c_str());
      char * result = adler(url);
      info.GetReturnValue().Set(Nan::New<String>(result).ToLocalChecked());
      free(result);
      free(url);
    }
    
    NAN_MODULE_INIT(InitAll) {
      Nan::Set(
        target, New("adler").ToLocalChecked(),
        Nan::GetFunction(New<FunctionTemplate>(adler)).ToLocalChecked()
      );
    }
    
    NODE_MODULE(addon, InitAll)

    Впрочем, прежде чем это случилось, обнаружилась ещё одна штука. Библиотека у меня была скомпилирована как динамическая, а node-gyp требовал статическую. Поэтому, в Cargo.toml нужно поменять вот эту строку:

    crate-type = [«dylib»]
    на вот эту:
    crate-type = [«staticlib»]

    После чего опять надо откомпилировать:

    cargo build --release --target x86_64-pc-windows-msvc

    Кроме того, надо не забыть теперь поменять путь до библиотеки в bindings.gyp на lib-версию:

            "libraries": [
                "/path/to/lib/adler.lib"
            ]

    И вот тогда-то всё должно собраться и получиться заветный файлик adler.node.

    В node опять меняем код для генерации хэша:

    var adler = require('/path/to/adler.node');
    for (var i=0; i<5000000; i++) {
        adler.adler("Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму");
    }
    

    Средний результат трёх запусков: 7,802 секунд. Лучший результат: 7,658

    О, это уже на пару секунд быстрее, чем даже нативный способ вычисления sha1! Выглядит очень даже симпатично!

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

    Кстати, что там у питона?


    В начале статьи упоминался python, почему бы и с ним тоже не попробовать, раз уж всё равно оказался под рукой? Там, как я уже говорил, adler32 можно посчитать прямо из коробки. Примерно такой вот будет код:

    # -*- coding: utf-8 -*-
    import zlib
    
    st = b'Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму'
    for i in range(5000000):
        hex(zlib.adler32(st))[2:]
    

    Средний результат трёх запусков: 2,100 секунды. Лучший результат: 2,072

    Нет, это не ошибка и запятая нигде не перепутана. По всей видимости, дело всё в том, что поскольку это часть стандартной библиотеки и по сути просто обёртка над Си-шным GNU zip, то это даёт преимущество в скорости. Иными словами, это сравнивается не Python и Rust, а Cи и Rust. И Си получается немного быстрее.

    UPD
    В Python тоже есть возможность использовать FFI, так что вот небольшое дополнение по этому поводу, по просьбе ynlvko.

    Понадобилось перекомпилировать библиотеку под win32, так как у меня стоит 32-битная версия python:

    cargo build --release --target i686-pc-windows-msvc

    Код:

    from ctypes import cdll
    
    lib = cdll.LoadLibrary("adler32.dll")
    for i in range(5000000):
        lib.adler(b'Какая-то не очень длинная строка, для которой надо посчитать контрольную сумму')
    Средний результат трёх запусков: 6,398 секунды. Лучший результат: 6,393
    То есть, получается, что питоний FFI работает в несколько раз более эффективно, чем node-ffi и даже эффективнее, чем «родные» аддоны

    Выводы


    Технология Среднее время, с Лучшее время, с
    Node.js 41,601 40,206
    Node.js+ffi+Rust 27,882 26,642
    Node.js (sha1) 9,737 9,321
    Node.js+C++Rust 7,802 7,658
    Python+ffi+Rust 6,398 6,393
    Rust 2,314 2,309
    C/Python (zlib) 2,100 2,072
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 82

    • UFO just landed and posted this here
        0
        Я пробовал низкоуровневые вещи написать на rust, все же он пока не готов, всплывают некоторые вещи, например проблемы с alloca.
          0
          rust в общем случае не должен уступать по скорости C

          Это почему это? В Rust используется bound checking при обращении к массивам по индексам, тогда как в C — нет. В ряде случаев Rust-овый компилятор способен избавиться от таких проверок (например, при итерациях), но не всегда.

          Плюс в Rust-е есть Drop-ы, которые аналоги плюсовых деструкторов, и Drop-ы должны вызываться при выходе из скоупа. Что так же не бесплатно.

          Плюс в Rust-е практикуется возврат Result-ов, т.е. пар значений. И в Result-е запросто может оказаться динамически созданный объект на месте Err. Что так же не дешево.

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

          Скорее в общем случае Rust должен хоть немного, но отставать от C.
            +7
            Что-то вы написали вроде и по делу, но не в тему. Абстракции в Rust точно так же как в С++ нулевой стоимости. Сам по себе компилятор не будет ни с того ни с сего добавлять сложность. Пары значений ни при каком раскладе к динамически создаваемому объекту не приведут. Только если вы явно их положили в кучу. В обычном случае же это будет или запись в стек или возврат в регистрах (гуглить по reg struct return). Дропы тоже будут использоваться только там где они реально нужны.

            Если уж сравнивать числодробительные возможности, то тогда надо смотреть на бенчмарки. Особенно забавно выглядит первая десятка в K-Nucleotide. И это притом что в Rust-е на данный момент еще только предстоит написать те оптимизации, которые будут на 100% пользоваться гарантиями его системы типов.
              –4
              как в С++ нулевой стоимости

              Абстракции в C++ далеко не нулевой стоимости.
              Пары значений ни при каком раскладе к динамически создаваемому объекту не приведут.

              Я этого и не утверждал. Только вот тот, кто вызывает метод f() не может знать, положит ли метод f() в результат простой Err или же это будет созданный динамически объект, который реализует нужный трайт.
              Дропы тоже будут использоваться только там где они реально нужны.

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

              Речь шла про общий случай, а не про числодробилки.
                +6
                Только вот тот, кто вызывает метод f() не может знать, положит ли метод f() в результат простой Err или же это будет созданный динамически объект, который реализует нужный трайт.

                Вот этого не может быть. Нельзя положить "какой-то объект реализующий трейт" по значению: размер объекта должен быть известен. Так что мы всегда увидим или конкретный тип или Box<Error>.

                  0
                  > Так что мы всегда увидим или конкретный тип или Box<Error>

                  И какой тип получается вот в этом примере из стандартной документации? Неужели Box<Error>?
                    +2

                    Нет.


                    Лучше смотреть вот сюда: parse возвращает конкретный (ассоциированный) тип из трейта FromStr. Какой именно — зависит от того, что (вернее, во что) парсим. Для bool это будет ParseBoolError и т.д.

                      0
                      Странно, у меня почему-то было ощущение (явно с чьих-то слов) о том, что в Result Err может быть именно что трейтом, а не конкретной структурой.

                      Возможно, ошибаюсь.
                        +3
                        Result<T, E> это практически тот же шаблон что в C++. Так что в принципе можно объявить любой тип в качестве E.

                        Возможно, вы обратили внимание на какой-то из вариантов, где использовались trait объекты.
                          +3

                          В расте, как и в С++, нельзя хранить по значению тип "переменного размера". T всегда будет конкретным типом, "интерфейс" надо представлять ссылками или указателями.


                          Положить трейт в Err можно, но это будет уже Result<T, Box<Error>>, в общем, из сигнатуры можно делать однозначные выводы.

                    +9
                    Вы похоже не понимаете, что такое zero cost abstraction.
                    Абстракции в C++ далеко не нулевой стоимости.
                    Если вам нужно вызвать функцию напрямую, то это будет ровно тот же самый call, что в C что в C++ что в Rust. Если вы заранее не знаете адресата (косвенный вызов), то опять же все три приведут одинаковой задержке. При всем желании C тут не сможет быть быстрее. Методы объектов Glib как пример.

                    В C++ может быть оверхед по объему кода при инстанциировании шаблона. То же самое и в Rust. Но это цена за возможность мономорфизации кода и оптимизаций при инлайнинге. В C шаблонов нет, но это не значит, что он будет быстрее в этом случае (инлайнинг в C++/Rust наоборот может оказаться быстрее). Наконец, и в C++ и в Rust можно написать код в plain C стиле и получить ту же производительность.

                    Я этого и не утверждал. Только вот тот, кто вызывает метод f() не может знать, положит ли метод f() в результат простой Err или же это будет созданный динамически объект, который реализует нужный трайт.
                    Неверно. Это записано в типе функции и известно статически уже на этапе компиляции.

                    Речь шла про общий случай, а не про числодробилки.
                    Вы говорите про общий случай, но упоминаете почему-то частности.

                    Наличие паник подразумевает, что должен быть какой-то механизм автоматического раскручивания Drop-ов при выбросе паники по аналогии с плюсовыми деструкторами и исключениями. Это не бесплатно, даже если паники не бросаются.
                    Опять мимо кассы. Паники построены с помощью механизма исключений LLVM на базе Itanium EABI. В отличие от setjmp/longjmp этот механизм не вносит задержек при нормальном развитии событий. Оверхед возникает только при фактической диспетчеризации исключения.
                      –5
                      Вы похоже не понимаете, что такое zero cost abstraction.

                      Да куда уж мне.
                      Вы говорите про общий случай, но упоминаете почему-то частности.

                      А вы подумайте, как эти частности скажутся в сумме.
                      Опять мимо кассы.

                      У табличного способа поиска исключений есть своя цена, даже если исключения не бросаются. Хотя бы в необходимости хранения этих самых таблиц.
                        +8
                        У табличного способа поиска исключений есть своя цена, даже если исключения не бросаются. Хотя бы в необходимости хранения этих самых таблиц.
                        Стоп, стоп. То мы говорили о скорости исполнения кода, а теперь уже о размере.

                        Скорее в общем случае Rust должен хоть немного, но отставать от C.
                        Я не говорю, что всегда и везде Rust будет быстрее. Нет конечно, его компилятор еще очень молодой и многих оптимизаций там просто нет. Но утверждать, что он будет медленнее в общем случае просто потому, что у него есть некие механизмы — нельзя.

                        Судить можно только на примере конкретной задачи, конкретной ее реализации и конкретного компилятора. Иначе это холивор.
                          –1
                          То мы говорили о скорости исполнения кода, а теперь уже о размере.

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

                          Ну вот в реальности C++, например, хоть чуть-чуть, но медленее C. Как раз потому, что абстракции на практике не бесплатны (чтобы там не говорили евангелисты). Нравится верить, что в Rust-е, не смотря на более высокий уровень абстракции, будет что-то по-другому, ну OK, нет проблем.
                            +2
                            Вот как раз верить во что-то, без каких либо оснований, это и есть настоящий фанатизм.

                            Я не предлагаю верить. Я предлагаю смотреть на конкретные примеры. А там код с абстракциями может уже быть как медленнее так и быстрее наивной реализации. It depends.
                              0
                              Вот как раз верить во что-то, без каких либо оснований, это и есть настоящий фанатизм.

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

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

                                Если говорить совсем точно, то C можно описать с помощью простого типированного лямбда исчисления (с дополнениями). На уровне типов он ничего особенного не предоставляет.

                                C++, являясь тьюринг полным и на уровне системы типов, чуть сложнее: его можно представить простым типированным λ-исчислением на уровне термов и простым нетипированным λ-исчислением на уровне типов. Нетипированное оно потому, что в С++ нет возможности задать ограничения параметры шаблонов. То что в комитете называют концептами и уже несколько версий не могут сделать.

                                Rust в этой иерархии стоит еще выше (если учитывать возможность полной специализации дженериков): у него есть ограничения на уровне типов. Поэтому при неверной попытке специализации шаблона (дженерика) Rust дает внятное сообщение об ошибке, а не пресловутые «три экрана шаблонов».

                                Круче только языки с продвинутыми системами типов на базе λ-исчисления высших порядков: Haskell, Coq, Agda и прочее.

                                Короче. Я веду к тому, что Rust, в отличие от C++, может грамотно распорядиться своей системой типов и инвариантами, которые из нее можно вывести. А это хорошая пища для оптимизатора. То есть, в тех случаях, когда компилятор C++ пасует и вынужден генерировать общий код, компилятор Rust сможет безопасно закодировать более производительный вариант, либо провести более агрессивный инлайнинг, векторизацию и т.п.

                                P.S.: Кому интересна эта тема, может посмотреть мой доклад на одной из конференций C++ Siberia.
                                  +2
                                  Синтетический пример:

                                  extern fn foo(input: &T);
                                  
                                  fn bar(input: &T) { ... }
                                  
                                  fn baz(input: &T) {
                                      foo(input);
                                      bar(input);
                                      foo(input);
                                  }
                                  

                                  Здесь в коде есть одна внешняя функция с неопределенным контрактом и две внутренние которые пользуются ссылкой на T.

                                  В Rust если функция принимает &T это значит, что объект на момент выполнения является замороженным и гарантировано не будет меняться где-то еще. Поэтому оптимизатор может с чистой совестью один раз прочитать значение из памяти и положить его в регистр (если это выгоднее и не создаст нежелательного register pressure).

                                  Функция bar() сможет быть заинлайнена внутрь baz() и пользоваться тем же значением из регистра, потому что компилятор может быть уверен, что input случайно не поменяется (interior mutability — отдельная история). То есть компилятору тут даже escape анализ проводить не нужно, чтобы это понять.

                                  Разумеется в C++ все сильно сложнее и в общем случае const T& не является достаточным основанием для подобных оптимизаций. Алиасинг указателей вообще больная тема.
                                    +1
                                    Тупанул. В теле baz() должно быть bar(); foo(); bar(), а не наоборот.
                                      +1
                                      потому что компилятор может быть уверен, что input >случайно не поменяется (interior mutability — отдельная >история)

                                      А почему можно так лихо убрать из рассуждений interior mutability, вполне может быть:


                                      struct T {
                                          data: RefCell<u32>,
                                      }
                                      
                                      fn foo(input: &T) {
                                          let mut data = input.data.borrow_mut();
                                          *data += 1;
                                      }
                                      

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

                                        0
                                        То, что внутри immutable объекта есть указатель по существу не сильно влияет. Само значение указателя, его можно сложить в регистр и не трогать. Можно так же инлайнить.
                                        Вроде бы…
                                          +3
                                          Все проще. Вводя unsafe, вы грубо говоря, подписываетесь под тем, что обеспечите все необходимые условия для корректной работы.

                                          Если вы объявили тип T как Send+Sync то это означает, что вы позаботитесь о синхронизации и видимости.

                                          В однопоточном случае это значит, что вы динамически проконтролируете баланс ссылок на объект. По этой причине RefCell кидает панику если при попытке взять мутабельную ссылку счетчик shared ссылок ненулевой.

                                          Подробнее можно почитать в документации и в номиконе.
                                        0
                                        Я веду к тому, что Rust, в отличие от C++, может грамотно распорядиться своей системой типов и инвариантами, которые из нее можно вывести.

                                        А может и не распорядиться и работать с трайтами, как с таблицей виртуальных функций, а так же выполнять все bound checks и возвращать через Result пользовательские enum-ы размером в сотни байт, при этом ничуть не избавляясь от цепочки if-ов, которые спрятаны за синтаксическим сахаром try! и?..
                                          +4
                                          Я вас в упор не понимаю.

                                          Я пытаюсь говорить о фундаментальных преимуществах, которые может дать система типов Rust, аналогов которых в С/C++ нет. Вы — про текущие недостатки реализации.

                                          Какой-то бессмысленный спор выходит.
                                            0
                                            Давайте вернемся к истокам спора. Началось все с фразы:
                                            rust в общем случае не должен уступать по скорости C

                                            На мой взгляд, это утверждение неверно. Т.к. в общем случае (а не тогда, когда какой-то код затачивается под максимальную производительность в микробенчмарке посредством ухода в unsafe) пользователи будут пользоваться преимуществами Rust-а, как то:
                                            — встроенные проверки выхода за пределы векторов;
                                            — более высокие уровни абстракции, т.е. trait-ы, которые в ряде случаев будут работать так же, как и виртуальные методы в обычных ОО-языках;
                                            — RAII через Drop, в том числе и при паниках;
                                            — возврат значений (в том числе и не маленьких) через Result, вместо использования привычных для C-шников параметров-указателей.

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

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

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

                                            Ну и да, речь не про фундаментальные преимущества, которые _могут_, а про постылую обыденность, в которой идеальный быстрый код пишут словами в камментах на профильных форумах.
                                              0
                                              RAII через Drop, в том числе и при паниках;

                                              Но ведь в C вы всё-равно будете освобождать ресурс тогда, когда он вам не нужен. Имеено об этом идёт речь, когда говорят о zero-cost abstractions. То есть, корректная программа на C не будет ничем быстрее корректной программы на, например, C++ с деструкторами. Просто если вы используете C вам придётся те же самые деструкторы вызывать в явном виде, что намного сложнее, или же и вовсе невозможно.

                                                0
                                                Не так все просто. Во-первых, в том же C++ деструкторы не всегда инлайнятся. Т.е. если где-то в коде написано что-то вроде:
                                                void f() {
                                                  File a(...);
                                                  ...
                                                  File b(...);
                                                  ...
                                                } // (1)

                                                а в каждом деструкторе делается вызов close(), то код с деструкторами будет чуть-чуть дороже, чем код с прямым вызовом close():
                                                void f() {
                                                  int a = open(...);
                                                  ...
                                                  int b = open(...);
                                                  ...
                                                  close(b);
                                                  close(a);
                                                }

                                                Во-вторых, запись действий по очистке вручную может позволить записать действия более компактно. Т.е. если в коде на C++ между вызовами деструкторов a и b пройдет вызов еще нескольких деструкторов, то данные и код для вызова деструктора b уже могут уйти из кэша. Тогда как в C может быть записано что-то вроде:
                                                void f() {
                                                  int a, b;
                                                  ...
                                                  a = open(...);
                                                  ...
                                                  b = open(...);
                                                  ...
                                                cleanup:
                                                  ... // Какие-то другие действия.
                                                  close(a);
                                                  close(b);
                                                }

                                                Так что на практике выплывают некоторые мелочи из-за которых незначительное преимущества в скорости у C все-таки образуются.

                                                Другое дело — стоят ли они того…
                                                  0
                                                  Во-первых, в том же C++ деструкторы не всегда инлайнятся.

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


                                                  Т.е. если в коде на C++ между вызовами деструкторов a и b пройдет вызов еще нескольких деструкторов, то данные и код для вызова деструктора b уже могут уйти из кэша.

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


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

                                                    0
                                                    Если они не инлайнятся — значит, компилятор считает, что так будет быстрее и вполне возможно, что он прав.
                                                    Или в принципе не может это сделать, т.к. они лежат в отдельной dll-е.
                                                    Какая разница, как это записать?
                                                    Ну если вы не увидели разницы между приведенными двумя примерами кода, значит ее нет, а я во всем неправ.
                                                      +1
                                                      Или в принципе не может это сделать, т.к. они лежат в отдельной dll-е.

                                                      Если у вас функция деинициализации лежит в отдельно библиотеке то вы и в C будете бессильны.


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

                                                      Ну я признаю, что если у вас перед вызовами close() идёт, например, особождения массива динамически выделенных объектов, то данные могут действительно уйти из кэша. Просто когда я представляю себе гипотетически такую ситуацию и представляю, что я пишу этот код на C, я понимаю что не знаю как его лучше написать. Вообще не представляю, какой вариант будет быстрее. И вряд ли вам кто-то скажет точно кроме серии бенчмарков. Отсюда вывод — потенциально вы может и можете написать на C более быстрый код. На практике, не изучая конкретный случай, компилятор, процессор — нет, не можете.

                                    +2
                                    Никто не против потерять 1% в скорости при гарантиях data safe. В этом и фишка.
                                      +1
                                      А разве кто-то против этого спорит? Иметь гарантии безопасности от Rust-а при разнице в скорости в районе процента-двух — это просто замечательно.
                                        +4
                                        Ну вот и спорить о том, какой конь сферичеснее в вакууме тоже не стоит.
                                        Rust — это самое прекрасное, что создала компиляторная индустрия за последние лет 10.
                                    +1
                                    То мы говорили о скорости исполнения кода, а теперь уже о размере.
                                    Ну справедливости ради размер кода влияет на его скорость его исполнения из-за наличия кеша.
                                      +4

                                      Landing pads же не разбавляют каждый метод, равномерно его раздувая. А будет call/ret на 12k от текущей позиции или на 13k — разницы практически никакой.


                                      Ну и для желающих от них полностью избавиться, можно запретить panic'и (использовать panic = abort вместо panic = unwind).

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

                                        Я потому и говорю, что не существует никакого «общего случая». Всегда надо смотреть на конкретный пример.
                                          0
                                          Я так понимаю, просмотр таблиц идет только при панике? При нормальном развитии событий никакой потери производительности, при этом допущении, нет?
                                            0
                                            Да, это просто статические данные, как и строковые литералы. Только пожалуйста не забывайте, что сам механизм диспетчеризации исключения является очень медленным.
                                              0
                                              Первый вариант реализации нелокального возврата из функции в моей JIT VM был сделан с помощью выброса исключения. Для прототипа большего и не надо было, да и на первый взгляд, инструмент был самый подходящий: бросаем исключение в дебрях, в объекте исключения кодируем некоторым образом информацию о точке, докуда надо размотать стек и выбрасываем его. Остальное сделает автоматика.

                                              Потом на практике оказалось, что кумулятивно, одна только эта операция по скорости отбрасывала JIT код далеко назад даже по сравнению с софтовым исполнением! То есть буквально: нет инструкций blockReturn — JIT в 50 раз быстрее. Есть — настолько же медленнее.

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

                                              Подробнее можно тут почитать в описании релиза 0.4.
                                      +2
                                      … по аналогии с плюсовыми деструкторами и исключениями. Это не бесплатно, даже если паники не бросаются.

                                      The main model used today for exceptions (Itanium ABI, VC++ 64 bits) is the Zero-Cost model exceptions.… the Zero-Cost model, as the name implies, is free when no exception occurs

                                      http://stackoverflow.com/questions/13835817/are-exceptions-in-c-really-slow
                                      0
                                      К сожалению, пока не знаю Rust, но по ходу статьи читал о преобразованиях типов попутно при переложении алгоритма под Node.

                                      Я подозреваю, что большая потеря может происходить при перехода от v8 string -> bytes -> adler -> string -> bytes -> v8 complementary type.

                                      По крайней мере, я бы копал в направлении работать напрямую с входными байтами в представлении v8, отдавать родную строку в v8 (речь ведь о hex).
                                        0
                                        Попробовал вызвать библиотечную версию функции из раста — получил среднее время на уровне 3,5 секунд. То есть, издержки на преобразования достаточно большие. Но мне кажется. что даже если их убрать совсем, то разница между вызовом в рамках раста и по тому или иному интерфейсу всё равно будет раза в два. То есть, будет примерно как в питоне ±. Впрочем, это конечно всё равно привлекательно, поскольку получается, что даже современный весьма хорошо оптимизированный js на порядок медленнее, чем вызываемая раст-функция через все эти обёртки и преобразования.
                                      +3
                                      В ряде случаев Rust-овый компилятор способен избавиться от таких проверок (например, при итерациях), но не всегда.

                                      Там, где не может компилятор, но очень хочется, то можно сделать руками через get_unchecked.


                                      и Drop-ы должны вызываться при выходе из скоупа. Что так же не бесплатно.

                                      Дык, если нам надо выполнять какой-то код на выходе из скопа, то и в C код будет, только написанный руками.


                                      Насчёт динамических ошибок и диспатчинга: это всё делается руками, в зависимости от ситуации. Конечно, нет гарантий на то, что в библиотеках оно будет реализовано так, как нам хотелось бы, но ведь это и для С справедливо.


                                      Хотя с выводом я даже соглашусь с тем уточнением, что в общем случае код на расте будет безопаснее. (:

                                        –1
                                        Там, где не может компилятор, но очень хочется, то можно сделать руками через get_unchecked.

                                        Это будет не общий случай.
                                        что в общем случае код на расте будет безопаснее

                                        Это подразумевается по умолчанию. Если намеренно игнорировать безопасность Rust-а, смысла в его использовании нет.
                                          +1
                                          Это будет не общий случай.

                                          С этим не спорю, просто хотел уточнить, что если такое необходимо, то делается довольно легко.


                                          Если намеренно игнорировать безопасность Rust-а, смысла в его использовании нет.

                                          Как сказать. Если выбор между С и Rust, то даже в гипотетической ситуации, когда у нас код на 90% состоит из unsafe блоков, я бы всё равно предпочёл последний. Если, конечно, нет других факторов.

                                          +3

                                          Если не нужен произвольный доступ и известен size_hint, то итераторы тоже не будут проверять границы

                                        • UFO just landed and posted this here
                                            0
                                            Тогда забываем «в общем случае». В общем случае в Rust-е не будут использоваться unsafe-блоки и mem::forget.
                                            • UFO just landed and posted this here
                                                0
                                                Интересное определение общего случая. Тогда понятно, почему у вас в общем случае Rust имеет производительность C.
                                                • UFO just landed and posted this here
                                                    0
                                                    Ну логично при сравнении двух языков сравнивать те варианты использования языка, которые показывают лучший результат в каждом из них при одинаковой функциональности полученных программ. Если «лучший» значит «более производительный» (как в данном контексте), то в Rust есть способы писать код так, что бы он не уступал C по производительности. При этом, следующим вопросом может быть, будет ли такой производительной Rust-код проигрывать C-коду по другим критериям (удобство написания и поддержки, например). Я не знаю случаев, когда Rust-код будет уступать по этому критерию C-коду, хотя может они и есть.
                                                      0
                                                      Общий случай — это когда используются самые распространенные практики и приемы. Переход в unsafe в Rust-е для выжимания производительности вряд ли можно считать распространенной практикой.
                                                        +2
                                                        Расскажите пожалуйста, какова распространенная практика сортировки массива в С?
                                                          0
                                                          Подозреваю, что это намек на то, что обобщенные реализации sort-а быстрее qsort с косвенными вызовами.

                                                          Повторю то, что уже говорил:
                                                          При этом в Rust-е вполне можно, если задаться целью, получить и более быстрый код, чем на C (в C++ это так же возможно и продемонстрировано), но речь будет идти не про общий случай, а про частную задачу, вероятно, не очень большого объема.

                                                          Какой будет следующий намек?
                                                            +1
                                                            Зачем вы увиливаете от ответа?
                                                            Вы спорите с утверждением:
                                                            rust в общем случае не должен уступать по скорости C


                                                            Я вас спрашиваю, какая реализация сортировки в общем случае используется в С?
                                                            В расте в общем случае используется обобщенная. И что то мне подсказывает что в С в общем случае используется qsort который легко может оказаться на порядок медленнее. В результате раст не то что не уступает в общем случае, а оставляет С далеко позади.
                                                            Ну а если задаться целью, то да, можно и еще более быстрый код получить, но мы же не об этом сейчас?
                                                              +1
                                                              Зачем вы увиливаете от ответа?

                                                              Ответ вам был дан. Почему вы не можете его прочитать и понять?
                                                              Я вас спрашиваю, какая реализация сортировки в общем случае используется в С?

                                                              Тогда позвольте вас спросить: мы будем под общий случай выдавать конкретную ситуацию? Тогда давайте посмотрим на такой вот «общий» случай:
                                                              void f(char * b, size_t i) { b[i] = 0; }
                                                              

                                                              Значения b и i определяются в run-time, заранее они не известны.

                                                              Ну и, видимо, нужно более четко обозначить свою точку зрения, ибо очевидно, что для местных комментаторов она не очевидна:

                                                              — в Rust-е не так уж много особенностей, которые бы не позволили компилятору Rust-а сгенерировать такой же эффективный код, как и компилятору C. Одна особенность — это включенный по умолчанию bounds checking, вторая — это генерация таблиц для обработки panic (хотя это лишь косвенное влияние оказывает). А так, в принципе, у компилятора Rust-а достаточно информации, чтобы сгенерировать даже более оптимальный код;
                                                              — однако, производительность кода будет определяться не столько возможностями компилятора, сколько программистом. Rust является языком более высокого уровня, позволяет решать более сложные задачи, что дает возможность разработчику использовать более высокие уровни абстракции. Чем выше, тем меньше внимания уделяется тому, что внизу, откуда и проявляется некоторый проигрыш в производительности/ресурсоемкости по сравнению с более низкоуровневыми языками. Это не уникально для Rust-а, это уже проходилось, на моей памяти, как минимум с Ada, Eiffel и C++ (если говорить про то, что транслируется в нативный код);
                                                              — под общий случай лучше брать не какой-то микробенчмарк и уж тем более не часть микробенчмарка (вроде упомянутой вами сортировки), а решение какой-то большой задачи. Например, реализация MQ-шного брокера (хоть MQTT-брокера, хоть AMQP) или сервера СУБД. На таком объеме языки более высокого уровня сильно выигрывают в трудозатратах, качестве и надежности, но на какие-то проценты проигрывают в производительности тому же C (причины см. в предыдущем пункте). Если не верите, попробуйте пообщаться с разработчиками PostgreSQL или Tarantool-а.
                                                0
                                                от проверки границ можно избавиться при помощи арифметики указателей в unsafe-блоках

                                                К счастью, от проверки границ можно избавиться более простым способом. (:

                                            +3

                                            Интересный опыт, как насчет опубликовать это как npm пакет?

                                              0
                                              В принципе, это наверное можно сделать. единственное, я пока не совсем хорошо понимаю, как там правильно сделать кросс-платформенную сборку самим npm всего этого дела вместе с растовским кодом. Если для сборки с/с++ там есть node-gyp, то для сборки раста надо как-то cargo вызывать. Или затаскивать сразу под все платформы уже скомпилированные библиотеки? Но это как-то мне кажется перебор, ради такой плёвой функции. хотя, можно было бы подумать над тем, чтобы сделать какую-то обёртку, которая удобно подключала бы через аддоны библиотеки, совместимые с FFI. Но это немного другая уже история.
                                              0
                                              Если бы ещё было сравнение Python + Rust
                                                0
                                                Дополнил статью
                                                  0
                                                  Опять же, можно посмотреть бенчмарки и сравнить. Там и на питоне реализации есть.
                                                    0
                                                    Там сравнение скорости вычислений, и понятно, что у компилируемых языков или языков с JIT она будет быстрее.
                                                      0
                                                      Ну разумеется. А разве автор поста что-то другое делал? Хеширование — самая что ни на есть вычислительная задача.

                                                      Какое еще сравнение вы ожидаете увидеть?
                                                  0

                                                  Думал, что для nodejs есть https://github.com/neon-bindings/neon. Было бы любопытно взглянуть на его результат. Или это что-то другое?

                                                    0

                                                    neon — это несколько иная штука. Они предоставляют абстракции только для node.js. То есть, код который будет написан на расте с использованием neon — теряет универсальность. Библиотечка, которую я там написал, подключается и к node.js и к python и другим — по универсальному интерфейсу. Впрочем, есть смысл попробовать, возможно он был бы быстрее за счёт лучшей адаптированности под задачу

                                                      0
                                                      имхо не стоит использовать неон для подключения либы. Он скорее нужен когда надо ускорить уже существующий js код, то есть какой то кусок тупит, его надо вынести в раст, но при этом не потерять обвязку из формата объектов/классов. Чисто диванно-теоретическое имхо.
                                                    0

                                                    Код на Расте из статьи не компилируется.


                                                    pub extern "C" fn adler(url: *const c_char) -> c_char

                                                    Эта функция возвращает c_char, хотя s.into_raw() вернёт указатель на строку, а не отдельный символ. Нельзя возвращать указатель на строку, которая будет деаллоцирована до выхода из функции.


                                                    В C++ используется несовместимая сигнатура функции:


                                                    extern "C" char * adler(char * url);
                                                      0
                                                      Да, прошу прощение. Должно быть:

                                                      -> *mut c_char
                                                        0

                                                        При выходе из функции будет вызван деструктор для переменной s, в результате освободится паямять и функция вернёт указатель на освобождённую память.

                                                          0
                                                          Откровенно говоря, я ещё не до конца проникся всеми тонкостями работы с типами раста, но конкретно в данном случае, я опирался вот на этот пример: http://jakegoulding.com/rust-ffi-omnibus/string_return/
                                                          И именно в этом виде, с *mut оно компилируемая и работает. Если правильнее сделать как-то иначе, то будет любопытно узнать.
                                                            +4

                                                            Ошибся, деструктор здесь не будет вызван.


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


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

                                                              +1
                                                              Правильно ли я понимаю, что это касается вот этой части кода на с++
                                                              char * result = adler(url);
                                                                info.GetReturnValue().Set(Nan::New<String>(result).ToLocalChecked());
                                                                free(result);
                                                              

                                                              То есть, вот этот вот free — вызывать опасно?
                                                              Но вроде как тут как раз не должно быть разных аллокаторов именно по причине того, что код вызывается сначала в C++, а уже потом значение передаётся в ноду. А они используют, насколько я понимаю, один и тот же аллокатор. И в документации на этот счёт как раз сказано, что при компиляции в библиотеки всё должно работать корректно.
                                                              Dynamic and static libraries, however, will use alloc_system by default. Here Rust is typically a 'guest' in another application or another world where it cannot authoritatively decide what allocator is in use. As a result it resorts back to the standard APIs (e.g. malloc and free) for acquiring and releasing memory.

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

                                                                На Stack Overflow есть по этому поводу вопрос и ответ.


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

                                                                  0

                                                                  Если я правильно понял, там говорится про разные кучи, а не про разные аллокаторы. Впрочем, есть и другой момент — там идёт речь о том, как взаимодействуют библиотеки написанные на С/С++. И в этом смысле, у меня есть подозрение, что логика работы может отличаться. В данном случае, было бы разумно поставить под сомнение и то, что написано в документации и то, что написано на стэке. Существует ли воспроизводимый сценарий получить проблему? Если да, то можно было бы поставить какой-то эксперимент и посмотреть, что будет. Если откровенно, то мне вообще не вполне понятно, что именно должно в данном случае пойти не так.

                                                      –2
                                                      Выброси ты эту ноду, нагородил тут. Пошел бы лучше PR в Rocket сделал.
                                                        +2
                                                        Пусть они сначала своё API стабилизируют и переведут его на асинхронный режим работы. Они обещали это сделать в будущем. Кроме того, выкинуть-то я её может быть и выкинул бы, но кто мне старые проекты перепишет с ноды и питона на раст?

                                                        Тут я как раз описал некоторый такой вариант диффузной миграции. С его помощью можно постепенно наработать некоторый объём нужного мне функционала и как-то попривыкнуть к местным обычаям. А в какой-то момент просто окажется, что всё, что мне нужно — есть в расте и я понимаю как это эффективно использовать. Вот тогда-то можно будет и выкинуть что-то. И то, что-то всё равно останется, просто потому как есть проекты переменчивые, а есть в стиле «Работает — не трогай», которые я годами не обновляю, поскольку просто нет нужды. Зачем их переписывать? Что я с этого получу?

                                                        А главное, как я буду объяснять людям, которые работают со мной совместно, что я взял и выкинул ноду? Они-то раста не знают. Подключаемую библиотеку они ещё могут воспринять, а полный отказ от ноды будет означать отказ ещё и от их участия в проекте. К чему такой тоталитаризм?
                                                        0
                                                        Есть такой проект Neon https://github.com/neon-bindings/neon для написания модулей для ноды на rust. Можете тоже попробовать для интереса. Я пробовал (https://github.com/OrKoN/base-x-native) и получилось быстрее, чем js реализация для моего случая. Правда не сравнивал с другими нативными реализациями.
                                                          +1
                                                          А вы не пробовали сравнить скорость такого подключения модуля к nodejs и конвертации его в WebAssemply код?

                                                          Должно получиться интересно

                                                          Getting Started With WebAssembly in Node.js
                                                            +1
                                                            Да, это любопытно. Вчера как раз в «This Week in Rust» пиарили такую вот штуку: https://davidmcneil.github.io/the-rusty-web/

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

                                                          Only users with full accounts can post comments. Log in, please.