Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Спойлер: Rust быстрее и вообще смысл в этом. (отсылка на серию статей от @humbug)
Шутки шутками, но пришли мы сюда не за этим (говорю за себя). Не так давно один мой знакомый попросил меня написать FizzBuzz на Rust. Казалось бы, в чём проблема? Так вот проблема не в чём, а в ком - во мне. Получил я версию одновременно жутко лаконичную и ужасно вербозную, а заодно решил написать примерно идентичные (в плане работы логики, а не внешнего вида) реализации на нескольких других языках, а потом и измерить их производительность на среднем железе (Windows, 3,5 ГГц, 4 ГБ ОЗУ) додумался, дабы разобраться в возможности практического применения подобных решений. Вообщем, ближе к делу.
NOTE: Проводилось по пять запусков, но представлены только наименьшее и наибольшее время, а также среднее пропорциональное для всех пяти запусков.
Общая задача
Задача предельно проста - реализовать функцию fb
, которая будет возвращать "Fizz", "Buzz", "FizzBuzz" или "Other(x)", где x - тридцатидвухбитный знаковый аргумент. Вывести результаты выполнения fb
для чисел в последовательности с 1 по 100000. На некотором псевдокоде можно описать как:
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_millis() as f64 / 1000.);
}
Как же так! Чуть не забыли про виновника торжества! Код весьма сомнителен, ведь логики всего две строки (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) | 20 |
Python
from time import time
def fb(x: int) -> str:
fizz: bool = x % 3 == 0
buzz: bool = x % 5 == 0
if fizz and not buzz:
return 'Fizz'
elif buzz and not fizz:
return 'Buzz'
elif fizz and buzz:
return 'FizzBuzz'
else:
return f'Other({x})'
start = time()
for i in range(1, 100001):
print(fb(i))
print(f'100000 iterations in {time() - start} seconds')
Эх, питончик, питончик, до чего меня довела жизнь? Использую тебя в бенчмарках...
Команда запуска (компиляция не поддерживается)
py -m fbpy
Результаты
Наименьшее, с. | Наибольшее, с. | Среднее, с. | Размер, КБайт | Длина, линии |
6,891 | 24,966 | 15,979 | 287616 (CPython311) | 20 |
Заключение и рейтинг
Производительность
Место | Язык | Среднее время, с. |
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 | Swift | 16 |
2 | Dart | 19 |
3 | Kotlin | 20 |
4 | Python | 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.