Статьи

Рубин может быть быстрее с небольшим количеством ржавчины

ржавчина

Весной 2013 года Сэм Саффрон отправил fast_blank в GitHub. Это было расширение C, которое обеспечивало быструю альтернативу String#blank? ActiveSupport String#blank? метод, который возвращает, является ли строка только пробелом.

На Full Stack Fest 2015 Иегуда Кац выступил с докладом, в котором рассказал о своем расследовании переписывания fast_blank в Rust. Его первоначальная цель состояла в том, чтобы попробовать наивный однострочник (исключая его реализацию Buf ) и посмотреть, насколько он медленнее, чем оригинальное расширение C.

 extern "C" fn fast_blank(buf: Buf) -> bool { buf.as_slice().chars().all(|c| c.is_whitespace()) } 

К его удивлению, это было быстрее.

Что такое «ржавчина»?

Rust является одной из самых надежных попыток на сегодняшний день создать современный кроссплатформенный язык системного программирования.

Mozilla разработала Rust как язык, который может обеспечить производительность и безопасность, необходимые для современных веб-браузеров, и в то же время более доступный, чем предыдущие статические языки (по крайней мере, в некоторых отношениях). И это идет со славным сторонним менеджером пакетов! Вместо драгоценных камней у нас есть ящики .

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

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

Начиная

Если вы используете OSX, вы можете установить Rust с Homebrew . Убедитесь, что ваш Homebrew обновлен, потому что предыдущие версии формулы Rust не содержали cargo . Rust 1.4.0 используется здесь.

 $ brew install rust 

cargo — менеджер проектов / пакетов Rust. Начнем с создания исполняемого проекта:

 $ cargo new hello_world --bin 

Ржавым эквивалентом Gemfile является Cargo.toml :

 

Если мы посмотрим на src / main.rs, то увидим исходный код:

 fn main() { println!("Hello, world!"); } 

Давайте продолжим и скомпилируем проект:

 $ cargo build 

Это сгенерирует Cargo.lock, который должен обрабатываться так же, как Gemfile.lock .

 $ cargo run Hello 

Статическое Программирование

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

Основной глоссарий:

  • Тип данных — способ для компилятора определить диапазон адресов идентификатора
  • Указатель — идентификатор, связанный с адресом памяти
  • Псевдоним — когда два указателя указывают на одну и ту же память
  • Dangling Pointer — указатель указывает на свободную память
  • Утечка памяти — когда неиспользуемая память никогда не освобождается в течение жизни программы
  • Изменчивость — способность связывать различные значения с идентификатором в течение его жизненного цикла.

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

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

Скорее всего, если вы попытаетесь сделать что-то в Rust, что не безопасно для памяти, оно просто не скомпилируется. Фактически, независимо от того, что вы делаете, ваш код Rust, вероятно, не будет компилироваться.

Собственность и заимствование

Итак, как Rust обходится, не ожидая, что вы освободите память вручную?

Когда дело доходит до управления памятью, Rust в первую очередь занимается двумя вещами:

  1. Независимо от того, берет ли функция на себя ответственность или заимствует ее.
  2. Является ли ресурс изменчивым (хаотически нейтральным) или неизменным (законно хорошим).

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

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

Например, этот код будет работать:

 fn print_vec(v: Vec<i32>) { println!("{:?}", v); } fn main() { let v = vec![1,2,3]; print_vec(v); } 

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

 fn print_vec(v: Vec<i32>) { println!("{:?}", v); } fn main() { let v = vec![1,2,3]; print_vec(v); print_vec(v); } 

Мы получаем ошибку компиляции:

 src/main.rs:7:15: 7:16 note: `v` moved here because it has type `collections::vec::Vec<i32>`, which is non-copyable src/main.rs:7 print_vec(v); 

Это может быть решено путем передачи вектора для вызова функции с помощью & :

 fn print_vec(v: &Vec<i32>) { println!("{:?}", v); } fn main() { let v = vec![1,2,3]; print_vec(&v); print_vec(&v); } 

В настоящее время код не модифицирует вектор. Что происходит, когда мы пытаемся это сделать?

 fn print_vec(v: &Vec<i32>) { println!("{:?}", v); } fn main() { let v = vec![1,2,3]; v[0] = 0; print_vec(&v); print_vec(&v); } 

Мы получаем еще одну ошибку!

 src/main.rs:7:5: 7:6 error: cannot borrow immutable local variable `v` as mutable src/main.rs:7 v[0] = 0; 

Каждый раз, когда мы хотим изменить переменную в Rust, она должна быть указана как изменяемая:

 fn print_vec(v: &Vec<i32>) { println!("{:?}", v); } fn main() { let mut v = vec![1,2,3]; v[0] = 0; print_vec(&v); print_vec(&v); } 

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

Если вам нужен трассирующий сборщик мусора, есть Manishearth / rust-gc . Наконец, для сборщика циклов у вас есть fitzgen / bacon-rajan-cc .

Руби к ржавчине

Отличный способ увидеть, как работает Rust — взглянуть на него бок о бок с Ruby.

Определение функции

Рубин:

 def add(x, y) x + y end puts add(1, 2) 

Ржавчина:

 fn add(x: i32, y: i32) -> i32 { x + y } fn main() { println!("{}", add(1,2)); } 

Здесь -> i32 устанавливает тип возвращаемого значения функции, а i32 означает 32-разрядное целое число со i32 . {} является универсальным интерполятором строк в Rust и будет интерполировать остальные аргументы в println! макрос в порядке.

Динамические массивы

Ржавчина включает в себя Vec который может расти во время выполнения.

Рубин:

 names = ["bobby", "harry", "sally"] names << "thor" 

Ржавчина:

 fn main() { let mut names: Vec<&str> = vec!["bobby","harry","sally"]; // Alternatively // let mut names = vec!["bobby","harry","sally"]; names.push("thor"); } 

vec! макрос для создания экземпляров Vec

Затворы

Рубин:

 arr = [1, 2, 3] puts arr.map { |i| i * 2 }.inspect 

Ржавчина:

 fn main() { let arr = [1, 2, 3]; let mapped = arr.iter().map(|&x| x * 2); let output = mapped.collect::<Vec<i32>>(); println!("{:?}", output); } 

{:?} говорит интерполятору использовать для форматирования реализацию типа std::fmt::Debug trait (например, интерфейс Java или модуль Ruby mixin).

Лямбда

Рубин:

 is_even = ->(n) { n.even? } puts is_even.call(5) 

Ржавчина:

 fn main() { let is_even = |n: i32| n % 2 == 0; println!("{:?}", is_even(5)); } 

Классы и методы экземпляров

Рубин:

 class Triangle attr_accessor :base, :height def initialize(base, height) @base, @height = base, height end def area (@base * @height) / 2.0 end end triangle = Triangle.new(7, 5) puts triangle.area 

Ржавчина:

 struct Triangle { base: f32, height: f32 } impl Triangle { fn area(&self) -> f32 { (self.base * self.height) / 2f32 } } fn main() { let triangle = Triangle { base: 7f32, height: 5f32 }; println!("{}", triangle.area()); } 

В Rust struct представляет данные, а реализация представляет поведение.

Первый аргумент impl экземпляра impl — это особый случай self , &self или &mut self , в зависимости от необходимого уровня владения. Вы можете распознать передачу self методам экземпляра из Python.

Обратите внимание, что отсутствие точки с запятой в area является неявным возвращением.

Статические Методы

Рубин:

 module StaticMath def self.add(x, y) x + y end end puts StaticMath::add(1, 2) 

Ржавчина:

 struct StaticMath; impl StaticMath { fn add(x: i32, y: i32) -> i32 { x + y } } fn main() { println!("{}", StaticMath::add(1,2)); } 

В качестве альтернативы:

 struct StaticMath; impl StaticMath { fn add(&self, x: i32, y: i32) -> i32 { x + y } } fn main() { println!("{}", StaticMath.add(1,2)); } 

Статические методы («связанные методы» в Rust) создаются не указанием одного из self в качестве первого аргумента и последующим использованием :: вместо . вызвать метод. Хотя альтернативный пример работает, возможно, команда Rust не собиралась создавать статические методы таким образом.

Обратите внимание, что в Ruby либо :: либо . может использоваться для вызова любого метода, разница вводится в Rust.

Создание библиотеки

Давайте использовать наши знания Rust, чтобы создать быструю библиотеку и использовать ее на Ruby.

 $ cargo new libadd 

Нам нужно добавить несколько строк в Cargo.toml, чтобы Rust знал, что мы создаем динамическую библиотеку.

 [lib] name = "add" crate-type = ["dylib"] 

Обратите внимание, что dylib в crate-type не зависит от OSX. Это просто говорит Rust, что нам нужна динамическая библиотека, а не rlib (формат Rust), который используется по умолчанию.

Нам нужно добавить функцию, которую мы хотим выставить в src / lib.rs :

 #[no_mangle] pub extern "C" fn add(x: i32, y: i32) -> i32 { x + y } 

Теперь библиотека может быть скомпилирована:

 $ cargo build --release 

Ruby может общаться с Rust через интерфейс сторонних функций. Fiddle и FFI — это пара библиотек, которые позволяют нам делать это.

скрипка

fiddle поставляется с Ruby, поэтому вам не нужно ничего устанавливать.

 require "fiddle" require "fiddle/import" module Rust extend Fiddle::Importer lib_ext = "dylib" if `uname` =~ /Darwin/ lib_ext = "so" if `uname` =~ /Linux/ dlload "./libadd/target/release/libadd.#{lib_ext}" extern 'int add(int, int)' end puts Rust.add(1, 2) 

FFI

ffi — это драгоценный камень, который нужно установить.

 $ gem install ffi 

Это очень похоже на fiddle .

 require "ffi" module Rust extend FFI::Library lib_ext = "dylib" if `uname` =~ /Darwin/ lib_ext = "so" if `uname` =~ /Linux/ ffi_lib "./libadd/target/release/libadd.#{lib_ext}" attach_function :add, [:int, :int], :int end puts Rust.add(1,2) 

Вывод

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

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