Спойлер: Rust быстрее и вообще смысл в этом. (отсылка на серию статей от @humbug)
Шутки шутками, но пришли мы сюда не за этим (говорю за себя). Не так давно один мой знакомый попросил меня написать FizzBuzz на Rust. Казалось бы, в чём проблема? Так вот проблема не в чём, а в ком — во мне. Получил я версию одновременно жутко лаконичную и ужасно вербозную, а заодно решил написать примерно идентичные (в плане работы логики, а не внешнего вида) реализации на нескольких других языках, а потом и измерить их производительность на среднем железе (Windows, 3,5 ГГц, 4 ГБ ОЗУ) додумался, дабы разобраться в возможности практического применения подобных решений. Вообщем, ближе к делу.
NOTE: Проводилось по пять запусков, но представлены только наименьшее и наибольшее время, а также среднее пропорциональное для всех пяти запусков.
Общая задача
Задача предельно проста — реализовать функцию fb, которая будет возвращать «Fizz», «Buzz», «FizzBuzz» или «Other(x)», где x — тридцатидвухбитный знаковый аргумент. Вывести результаты выполнения fb для чисел в последовательности с 1 по 100 000. На некотором псевдокоде можно описать как:
func fb(x: int): String { var fizz = x % 3 == 0 var buzz = x % 5 == 0 if fizz and not buzz return "Fizz\n" else if not fizz and buzz return "Buzz\n" else if fizz and buzz return "FizzBuzz\n" else return "Other(${x})\n" } entry { var start = Time.now() for i = 1 to 100000 print(fb(i)) var elapsed = Time.since(start).as_milliseconds() / 1000.0 print("100000 iterations in ${elapsed} seconds\n") }
C
#include <stdio.h> #include <time.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> char* FB[] = {"", "Buzz", "Fizz", "FizzBuzz"}; char* fb(char *s, int x) { bool fizz = x % 3 == 0; bool buzz = x % 5 == 0; if (!fizz && !buzz) { sprintf(s, "Other(%d)", x); } else { strcpy(s, FB[fizz << 1 | buzz]); } return s; } main() { clock_t start = clock(); char s[15] = {0}; for (int i = 1; i <= 100000; i++) { puts(fb(s, i)); } printf("100000 iterations in %f seconds\n", (float) (clock() - start) / CLOCKS_PER_SEC); }
Код прозрачен как слёзы младенца, что его писал. Мало отличается от исходного, но интересен тем, что вместо тривиального подхода, связанного с простым перебором вариантов, мы используем битовые сдвиг и или для выбора нужной строки из уже готового буффера. Большое спасибо@rgimadза помощь с оптимизациями.
Команда компиляции
gcc fbc.c -Ofast
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
6,687 | 17,928 | 9,627 | 90 | 27 |
C++
#include <iostream> #include <chrono> #include <string> #include <format> #include <ranges> auto fb(int x) -> std::string { bool fizz = x % 3 == 0; bool buzz = x % 5 == 0; if (fizz && !buzz) { return "Fizz"; } else if (!fizz && buzz) { return "Buzz"; } else if (fizz && buzz) { return "FizzBuzz"; } else { return std::format("Other({})", x); } } auto main() -> int { std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); for (int i : std::ranges::views::iota(1, 100001)) { std::cout << fb(i) << std::endl; } std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); std::cout << std::format( "100000 iterations in {} seconds", (double) std::chrono::duration_cast<std::chrono::milliseconds>(end - start) .count() / 1000.0) << std::endl; }
Многие плюсовики винят меня в том, что мой код на плюсах — на самом деле код на си, так что на этот раз я решил представить максимально современное решение, использующее фичи С++20.
Команда компиляции
g++ fbcpp.cpp -Ofast --std=c++20
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
18,179 | 27,418 | 21,606 | 16310 | 31 |
D
import std.format: format; import std.stdio; import std.range : iota; import std.algorithm; import std.datetime : MonoTime; string fb (int x) { bool fizz = x % 3 == 0; bool buzz = x % 5 == 0; if (fizz && !buzz) { return "Fizz"; } else if (!fizz && buzz) { return "Buzz"; } else if (fizz && buzz) { return "FizzBuzz"; } else { return format("Other(%d)", x); } } void main() { auto start = MonoTime.currTime(); iota(1, 100001) .map!(fb) .each!(writeln); writefln("100000 iterations in %f seconds", (MonoTime.currTime() - start).total!"usecs"() / 1000000.0); }
Решение на ди выглядит как идеальная версия решения на плюсах. Оно мне невероятно нравится, написание доставило огромное удовольствие.
Команда компиляции
dmd fbd.d -O -release
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
8,034 | 18,79 | 10,043 | 730 | 27 |
Fortran
subroutine fb(i) integer, intent(in) :: i logical :: fizz, buzz fizz = mod(i, 3) == 0 buzz = mod(i, 5) == 0 if (fizz .and. .not. buzz) then write(*, '(a)') "Fizz" else if (.not. fizz .and. buzz) then write(*, '(a)') "Buzz" else if (fizz .and. buzz) then write(*, '(a)') "FizzBuzz" else write(*, '(a, i5, a)') "Other(", i, ")" end if end subroutine fb program main integer(kind = 4) :: i, start, end real(kind = 4) :: elapsed call SYSTEM_CLOCK(start) do i = 1, 100000 call fb(i) end do call SYSTEM_CLOCK(end) elapsed = end - start write(*, '(a, f7.4, a)') "100000 iterations in ", elapsed / 1000.0, " seconds" end program main
Вот и старичок‑фортран подъехал! Признаться, написание кода на этом немолодом красавце было не самым простым, но довольно приятным и интересным процессом.
Команда компиляции
gfortran fbf.f08 -fimplicit-none -Ofast
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
4,531 | 13,438 | 6,256 | 1781 | 27 |
Rust
#[derive(Debug)] #[repr(u16)] enum FB { Fizz = 1, Buzz = 256, FizzBuzz = 257, Other(i32) = 0, } fn fb(x: i32) -> FB { unsafe {core::mem::transmute((x % 3 == 0, x % 5 == 0, x))} } fn main() { let start = std::time::Instant::now(); (1..=100000) .map(fb) .for_each(|x| println!("{x:?}")); println!("100000 iterations in {} seconds", start.elapsed().as_secs_f64()); }
Как же так! Чуть не забыли про виновника торжества! Код весьма сомнителен, ведь логики всего две строки (1, 11), а бойлерплейта‑то сколько... Несмотря на то, что внешне это решение значительно отличается от решения на си, алгоритм тут такой же — смещение, только неявное, выполняемое на строке 11, на строке 1 же выполняется форматирование.
Команда компиляции
cargo build -r
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
6,815 | 19,751 | 9,878 | 186 | 20 |
Swift
func fb(x: Int) -> String { switch (x % 3 == 0, x % 5 == 0) { case (true, false): return "Fizz" case (false, true): return "Buzz" case (true, true): return "FizzBuzz" default: return "Other(\(x))" } } let elapsed = ContinuousClock().measure { (1...100000) .map(fb) .forEach({(i) -> Void in print(i)}) } print("100000 iterations in \(elapsed) seconds")
Свифт решил навестить своего старшего брата, а заодно и посоревноваться с ним. Решение невероятно элегантно и красиво, браво.
Команда компиляции
swiftc fbswift.swift -O -static
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
8,073 | 26,898 | 10,898 | 22 | 16 |
Go
package main import ( "fmt" "time" ) func fb(x int) string { fizz := x % 3 == 0 buzz := x % 5 == 0 if fizz && !buzz { return "Fizz" } else if !fizz && buzz { return "Buzz" } else if fizz && buzz { return "FizzBuzz" } else { return fmt.Sprint("Other(", x, ")") } } func main() { start := time.Now(); for i := 1; i <= 100000; i++ { fmt.Println(fb(i)) } elapsed := time.Since(start); fmt.Printf("100000 iterations in %f seconds", float64(elapsed.Nanoseconds()) / 1000000000.0) }
Несмотря на всю мою неприязнь к Go, не могу сказать, что мне было неприятно писать этот код, и даже наоборот.
Команда компиляции
go build fbgo.go
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
5,912 | 16,001 | 11,636 | 1910 | 30 |
PascalABC.NET
function fb(x: integer): string; begin var (fizz, buzz) := (x mod 3 = 0, x mod 5 = 0); if fizz and not buzz then result := 'Fizz' else if not fizz and buzz then result := 'Buzz' else if fizz and buzz then result := 'FizzBuzz' else result := string.format('Other({0})', x); end; begin milliseconds(); range(1, 100000) .select(fb) .println(char(10)); writelnformat('100000 iterations in {0} seconds', millisecondsdelta() / 1000.0); end.
PascalABC.NET — уникальный язык. Он сочетает в себе черты императивного, объектно‑ориентированного и функционального программирования в тех пропорциях, которых от подобного языка не ждёшь.
Команда компиляции
pabcnetc fbpas.pas
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
8,282 | 19,491 | 10,011 | 38 | 20 |
Dart
String fb(int x) { bool fizz = x % 3 == 0; bool buzz = x % 5 == 0; if (fizz && !buzz) return "Fizz"; else if (!fizz && buzz) return "Buzz"; else if (fizz && buzz) return "FizzBuzz"; else return "Other($x)"; } void main() { final start = Stopwatch()..start(); for (int i = 0; i <= 100000; i++) print(fb(i)); print("100000 iterations in ${start.elapsedMilliseconds / 1000.0} seconds"); }
Dart — замечательный скриптовой язык, мне он сразу понравился, не зря самый популярный GUI‑фреймворк — Flutter — предназначен именно для него.
Команда компиляции
dart compile exe fbdart.dart
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
9,013 | 28,216 | 13,132 | 4837 | 19 |
PHP
<?php function fb($x) { $fizz = $x % 3 == 0; $buzz = $x % 5 == 0; if ($fizz && !$buzz) { return "Fizz\n"; } else if (!$fizz && $buzz) { return "Buzz\n"; } else if ($fizz && $buzz) { return "FizzBuzz\n"; } else { return sprintf("Other(%d)\n", $x); } } $start = hrtime(true); foreach (range(1, 100000) as $i) { echo fb($i); } echo sprintf("100000 iterations in %f seconds\n", (hrtime(true) - $start) / 1e+9); ?>
Пых‑пых‑пых...
Команда запуска (компиляция не поддерживается)
php -f fbphp.php
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
5,497 | 26,465 | 11,459 | 85052 (PHP8) | 19 |
Kotlin
inline fun fb(x: Int): String { val fizz = x % 3 == 0 val buzz = x % 5 == 0 if (fizz && !buzz) return "Fizz" else if (!fizz && buzz) return "Buzz" else if (fizz && buzz) return "FizzBuzz" else return "Other($x)" } fun main() { val elapsed = kotlin.system.measureTimeMillis { for (i in 1..100000) println(fb(i)) }.toDouble() / 1000.0 println("100000 iterations in $elapsed seconds") }
Эх, Котлин... любовь с первого взгляда... мы были так молоды, а вся эта страсть...
Команда компиляции
kotlinc-native -opt fbkt.kt
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
13,666 | 16,69 | 15,302 | 540 | 20 |
Java
import java.util.stream.IntStream; class Main { public static void main(String[] args) { FB fb = new FB(); double start = (double) System.currentTimeMillis(); IntStream range = IntStream.range(1, 100001); range.forEach(x -> fb.fb(x)); double end = (double) System.currentTimeMillis(); double elapsed = (end - start) / 1000.0; System.out.println(String.format("100000 iterations in %f seconds", elapsed)); } } class FB { public static void fb(int x) { FB fizzbuzz = new FB(); System.out.println(fizzbuzz.inner(x)); } public static String inner(int x) { boolean fizz = x % 3 == 0; boolean buzz = x % 5 == 0; if (fizz && !buzz) { return "Fizz"; } else if (!fizz && buzz) { return "Buzz"; } else if (fizz && buzz) { return "FizzBuzz"; } else { return String.format("Other(%d)", x); } } }
Куда уж без блудной матери Котлина? Что же, поглядим на неё в деле...
Команда запуска (компиляция не поддерживается)
javac fbjava.java java Main
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
8,579 | 36,771 | 23,131 | 305767 (JRE11) | 34 |
Python
from time import time def fb(x: int) -> str: return "Fizz" * (not x % 3) + "Buzz" * (not x % 5) or f'Other({x})' start = time() for i in range(1, 100001): print(fb(i)) print(f'100000 iterations in {time() - start} seconds')
Эх, питончик, питончик, до чего меня довела жизнь? Использую тебя в бенчмарках...
Спасибо LeeeeT за идиоматичную fb.
Команда запуска (компиляция не поддерживается)
py -m fbpy
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
6,891 | 24,966 | 15,979 | 287616 (CPython311) | 11 |
Заключение и рейтинг
Производительность
Место | Язык | Среднее время, с. |
1 | Fortran | 6,256 |
2 | C и Rust | 9,627 и 9,878 |
3 | PascalABC.NET, D и Swift | 10,011, 10,043 и 10,898 |
4 | PHP и Go | 11,459 и 11,636 |
5 | Dart | 13,132 |
6 | Kotlin и Python | 15,302 и 15,979 |
7 | C++ | 21,606 |
8 | Java | 23,131 |
Размер бинаря
Место | Язык | Размер, КБайт |
1 | Swift | 22 |
2 | PascalABC.NET | 38 |
3 | C | 90 |
4 | Rust | 186 |
5 | Kotlin | 540 |
6 | D | 730 |
7 | Fortran | 1781 |
8 | Go | 1910 |
9 | Dart | 4837 |
10 | C++ | 16310 |
11 | PHP (PHP8) | 85052 |
12 | Python (CPython311) | 287616 |
13 | Java (JRE11) | 305767 |
Лаконичность (учитываются не только строки)
Место | Язык | Строки |
1 | Python | 11 |
2 | Swift | 16 |
3 | Dart | 19 |
4 | Kotlin | 20 |
5 | Rust | 20 |
6 | PascalABC.NET | 20 |
7 | D | 27 |
8 | Go | 30 |
9 | C | 27 |
10 | Fortran и C++ | 27 и 31 |
11 | Java | 34 |
Удобство форматирования (учитываются не только символы)
Место | Язык | Доп. символы |
1 | Rust | 0 |
2 | Kotlin и Dart | 1 |
3 | Python и Swift | 3 |
4 | D и PHP | 12 |
5 | Go | 16 |
6 | C++ | 17 |
7 | Java | 19 |
8 | PascalABC.NET | 20 |
9 | C | 24 |
10 | Fortran | 27 |
Некоторые результаты меня удивили, другие же я предугадал. Более всего меня удивила столь низкая позиция плюсов. Стёб стёбом, а мне теперь даже как‑то стыдно.
Подведём итоги:
Rust лучше всех;
PascalABC.NET умеет удивлять;
Python лаконичный, но медленный;
C даёт много простора воображению;
Dart, хоть и скриптовой, очень быстр и красив;
Kotlin немного подводит, но от того не перестаёт быть прекрасным;
D на высоте, как и всегда;
C++ подкачал... ну ничего, оклемается, Трупостраус же сказал);
Go — простенький, но шустренький;
Fortran — дед, хоть не разбирается в молодёжной эстетике, всё же может задать жару даже самым быстрым из современных языков;
Java уже не та;
PHP — соник скриптового мира;
Swift не зря так любим разработчиками, Крис Латтнер и Грейдон Хор отлично над ним поработали.
Прошу помнить, что бенчмарки субъективны, не стоит принимать всё близко к сердцу, первоочередная цель создания этого контента — развлечение.
Miiao.
