Pull to refresh

Comments 52

Ага, вот из-за наличия yes и приходится во всех критичных к вниманию юзера программах проверять на isatty(0) и читать прямо из /dev/pty вместо stdin :).

Надеюсь, у вас же правда есть какой-то параметр --force в таком случае?

Смотря что за скрипт. У некоторых (у полу-одноразовых скриптов, которые действительно предполагается только руками запускать и никак иначе) такого флага нет и даже скрытой возможности запустить из скрипта тоже нет. Но обычно есть какой-то способ запустить её в батч-режиме, да. Иногда это что-то вроде значения типа "i_know_that_i_am_doing_very_bad_thing_and_i_read_documentation" в виде проверки md5(param) = "0d52fbfcafbb4f29983fff89e4184904", где само значение действительно зарыто в документации и его человек может найти, только прочитав документацию достаточно и поняв её.

Просить ввести рандомную строку из 10 символов.
Кажется, вы в одном шаге от изобретения велосипеда капчи. Кстати, гугловая её реализация (та самая, где «поставьте галочку, если вы не робот») ни разу не тривиальна.
И анализировать их на этропию, если плохая — строка недостаточно рэндомная!

это не так сложно:


RANDOM_STRING=`xxd -p -l 5 /dev/random`
format C:? Yes! :)

Не очень понял, зачем этой программе такое мегабыстродействие?
Ну логично же, если ооочень много yes поставить нужно, то такая программа, будет крайне мало времени отъедать, в отличии от реализации на питоне например.

Страшно представить программу которой требуется 3 гигабайта в секунду подтверждений и работает она быстрее чем получает эти подтверждения...

У меня промелькнула мысль про какой-нибудь странный rm с рекурсивом но без форса, которому, при определённых обстоятельствах понадобилось бы очень-очень много раз жать y… Но, блин, 3 Гб? Даже отдалённо не получается придумать задачу ).
Это обычная разминка для ума, ясень пень, что не нужная такая скорость.
После чего особо хитрым юзерам вместо простых пайпов приходится использовать expect, что бы всетаки обернуть программу и автоматизировать ввод.)
Переработка стандартных инструментов Unix — увлекательное занятие и оно заставляет ценить те изящные трюки, которые делают наши компьютеры быстрыми

Теперь буду говорить на кодревью: "Это не костыль, это изящный трюк!"

Оптимизации для Бога Оптимизации! Производительности для Трона Производительности!

На удивление, наивная реализация (и скомпилированная безо всяких оптимизаций) на Хаскелле выдаёт 158MiB/s на рабочем десктопе:
module Main where
main :: IO ()
main = do
  putStrLn yes

yes :: String
yes = 'y' : '\n' : yes

Правда, встроенная `yes` показывает 3.8GiB/s, но это мелочи жизни=)

Твоя программа не реализовывает спецификацию утилиты yes. В статье написано ведь, что утилита должна принимать аргумент коммандной строки, заменяющий "y" в случае наличия.

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


module Main where

import Data.Function (fix)
import System.Environment (getArgs)

main :: IO ()
main = do
  args <- getArgs
  let str = if null args then "y" else head args
  putStr $ fix $ \s -> str ++ "\n" ++ s 
yes | sh boring_installation.sh

А почему тут yes прекращает свой бесконечный цикл и передает управление следующему скрипту в конвейере?
Они работают «параллельно», а между ними пайп, имеющий ограниченную емкость. Как только пайп заполнен («следующий» скрипт не читает из него), yes будет просто блокироваться на операции записи.
Ах вот как, они работают параллельно сразу, спасибо!
Я всегда думал, что они работают последовательно, сначала первая команда запустится и завершится, запишет выходные данные, затем вторая запустится и прочитает входные данные.
&& — вторая команда выполнится только если статус выхода из первой равен нулю
|| — вторая команда выполнится только если статус выхода из первой отличен от нуля
Да, но в этом случае команды не связаны конвейером.
Вывод первой не подается на вход второй.
Я всегда думал, что они работают последовательно, сначала первая команда запустится и завершится, запишет выходные данные, затем вторая запустится и прочитает входные данные.
Дык эта. Времена MS DOS давно прошли…
Впечатляющее количество строк у оптимизированной Rust версии для столь простой задачи.

Детально не разбирал, но с виду похоже на логику работы gnu версии на Си.

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

use std::borrow::Cow;
use std::io::{self, Write};

fn main() {
    const BUFFER_CAPACITY: usize = 32 * 1024;

    let expletive = std::env::args().nth(1)
        .map(|s| Cow::Owned(s + "\n"))
        .unwrap_or(Cow::Borrowed("y\n"));

    let buffer = expletive.repeat(BUFFER_CAPACITY / expletive.len());
    let stdout = io::stdout();
    let mut handle = stdout.lock();

    loop {
        handle.write(buffer.as_bytes()).unwrap();
    }
}


К тому же, этот вариант, на моей машине, быстрее на 7% быстрее.
Ничего не понятно.
У меня при комбинации yes и pv, полученных из репы Ubuntu 16.04 и запущенных как есть,
yes | pv -r > /dev/null

на Lenovo T60 (дрова, ддр2 и T7200) = 1,92 GiB
на Dell M6700 (ddr3 и 3632QM) = 7.58 GiB

А автор до 3 ГБ/с c оптимизированным кодом м трудом добрался.

Что я делаю не так или что я не понял в задаче?

Есть подозрение, что всё очень сильно зависит от частоты памяти. Я скомпилировал свою наивную реализацию на разных машинах и получил разные (в разы, а то на порядки) результаты по скорости.

Не всё так просто, по-моему. На 7-летней давности ноутбуке получаем 7GIB/s:


% yes |pv >/dev/null
^C.6GiB 0:00:05 [7.08GiB/s] [    <=>                                            ]

Model name:            Intel(R) Core(TM) i7-2640M CPU @ 2.80GHz
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              4096K

Linux 4.13.0-0.bpo.1-amd64

        Type: DDR3
        Speed: 1333 MHz

На достаточно новом сервере получаем 0.1GIB/s:


% yes |pv >/dev/null 
^C76GiB 0:00:27 [ 108MiB/s] [                                                                        <=>                                                            ]

Model name:            Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              25600K

Linux 4.4.79-1-pve

        Type: DDR4
        Speed: 2133 MHz
О как, интересно. У меня больше гипотез тогда нет=)
yes | pv > /dev/null, 1.6GiB 0:00:06 [ 1.9GiB/s]
celeron n3050, ddr 3

Неужели бывают установщики, которые СТОЛЬКО раз просят у пользователя подтвердить очередную операцию?

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

Исходники cat:
static void
raw_cat(int rfd) { … }

static void
cook_cat(FILE *fp) { … }

Весь файл на Гитхабе.

Очень интересно описано, но поправьте меня, если не прав, pv считает, сколько через нее прошло (слово "прошло" — условно) за секунду. Если мы отправляем один символ "y", то это около одного символа, а если отправляем буфер, то это около 8k символов — разница почти на 4 порядка по pv скорее показывает, что оба примера работают с одинаковой скоростью в контексте "количества выданных y"

Как выясняется, такая программа работает довольно медленно.


А на третьем питоне еще на 0.5 мб/с медленнее, чем на 2.7 :)
Стало интересно и решил посмотреть что-же получиться на Го.
Получилось так
package main

import (
	"bufio"
	"fmt"
	"os"
)

var bufsize = 64 * 1024

func main() {
	y := byte('y')
	n := byte('\n')
	buf := make([]byte, 0x1000)
	fmt.Println(bufsize)
	for i := 0; i < len(buf)-2; i += 2 {
		fmt.Println(i)
		buf[i] = y
		buf[i+1] = n
	}
	f := bufio.NewWriterSize(os.Stdout, bufsize)
	defer f.Flush()
	for {
		f.Write(buf)
	}
}


Результат:
$ ./yes| pv -r > /dev/null
[9.59GiB/s]


Системный yes выдаёт
$ yes | pv -r > /dev/null
[9.06GiB/s]
Пофикшенная версия с опциональным аргументом
package main

import (
	"bufio"
	"os"
)

var bufsize = 64 * 1024

func main() {
	var y []byte
	// Get arg
	if len(os.Args) > 1 {
		y = []byte(os.Args[1] + "\n")
	} else {
		// Set output to y
		y = []byte("y\n")
	}
	yLen := len(y)

	// Create buffer
	buf := make([]byte, bufsize)
	// Popoulate buffer
	for i := 0; i < len(buf)-yLen; i += yLen {
		for s := 0; s < len(y); s++ {
			buf[i+s] = y[s]
		}
	}

	// Create buffered writer
	f := bufio.NewWriterSize(os.Stdout, bufsize)
	defer f.Flush()
	for {
		f.Write(buf)
	}
}



скорость не поменялась (да и с чего-бы? :) )
Ах вот откуда прут эти «y» когда Ansible playbook в начале просит познакомится с «новыми» хостами и я как макака вбиваю «yes» и каким-то образом один из prompt-тов плэйбука просачивается в shell и запускает yes утилиту. А я на Ansible гнал. Хотя кто-то же виноват в не санкционированном просачивании в shell.
Sign up to leave a comment.

Articles