Go Quick Clone "yes"사용

GNU coreutils Go 기반 클론 시리즈를 만든 후, 오늘은 yes 로 속도 실험을 하고 싶습니다.

YES란 무엇인가?


만약 맥이나 linux를 사용한다면, 터미널로 이동해서 yes 를 입력하십시오.너는 반드시 무한류를 얻어야 한다y.또는 yes yes로 실행할 수도 있습니다. yes의 무한 흐름을 얻을 수 있습니다.또는 yes no.
이 도구의 그래픽 표현:

이것은 무슨 의의가 있습니까?


만약 당신이 매우 긴 설치 스크립트를 가지고 있다면, 아무도 참여하지 않는 상황에서 실행하기를 원합니다.만약 스크립트에 모든 문제에 긍정적인 답안이 있도록 강제하는 표지가 있다면, 문제는 없다.하지만 시나리오가 매번 문제와 답안y이나 n을 기다리게 한다면 어떻게 해야 하나요?만약 네가 개의치 않는다면, 네가 모든 질문에 대답할 줄 알기 때문이냐?그리고 너는 할 수 있다.
$ yes | myInstallScript.sh
따라서 일정한 y 흐름이 파이프를 통해 설치 스크립트로 전송되기 때문에 어떤 질문을 받을 때마다 파이프가 해답을 해 줍니다.
또 다른 용도는 가상 데이터를 생성해야 한다면 필요에 따라 파이프y를 파일로 전송할 수 있다는 것이다.

그래서true인 경우 yes를 인쇄합니까?


약간, 하지만 전부는 아니다. 왜냐하면 우리는 성능에 관심을 가지기 때문이다.만약 우리가 개의치 않는다면, 우리는 할 수 있다.
expletive := os.Args[1]
if expletive == "" {
    expletive = "y"
}
for {
    fmt.Println(expletive)
}
이것이 바로 이야기의 결말이다.
그러나 이 실현이 얼마나 빠른지 가늠해 봅시다.만약 맥을 사용하고 있다면, yes 을 통해 계속 설치하십시오 pv.linux에서, 당신은 그것을 가지고 있을 수도 있습니다.Windows에서는 Cygwin을 사용해야 할 수도 있습니다.Pv는 파이핑을 통해 데이터를 측정하는 도구입니다.

실현 1: 무한 순환 인쇄


go build yes.go
./yes | pv > /dev/null
평균적으로 인쇄됩니다 [1.31MiB/s]
맥(BSD 구현)에 설치된 리얼brew install pv이 대략[23.4MiB/s]인 것을 감안하면 우리의 첫 번째 실현은 좋지 않다는 결론이다.

실현 2: 1억 장을 인쇄할 수 있는데 왜 한 장만 인쇄합니까


지금까지 우리는 줄곧 하고 있다
for {
    fmt.Println(expletive)
}
이것은 단지 하나하나 인쇄 순환일 뿐이기 때문에 그다지 효과가 없는 것 같다.중복되는 문자열을 많이 인쇄하여 흥미를 더합니다.
fmt.Println(strings.Repeat(expletive+"\n", 100))
이것은 우리에게 [89.8Mb/s]를 주었다.많이 좋아졌어!우리는 이미 맥 컴퓨터에 설치된 원시 버전 yes 의 기준 테스트를 통과했다.
왜 100에 멈춰 있어요?
fmt.Println(strings.Repeat(expletive+"\n", 100000000))
이로 인해 [1.37GiB/s]우리는 이곳에서 뭔가를 찾은 것 같다.1억 번이 넘는 중복은 적어도 내 컴퓨터에서 두 가지 일이 발생할 것이다. 차이가 없거나, 전혀 작용하지 않거나, 쓸 수 없다.

3: Goroutines 조립 실현!


사람들은 이곳에서 즐거움이 도움이 될 것이라고 생각할 수도 있다.만약 yes 여러 개의 goroutine에 고속으로 던져진다면, 이것은 높은 흡수량을 가져오지 않겠는가?나 몰라!저희가 발견할 거예요!
문제는 우리가 단지 이렇게 했을 뿐이라면:
yesChan = make(chan string)
go func() {
    yesChan <- strings.Repeat(expletive+"\n", 100000000)
}()
go func() {
    fmt.Println(<-yesChan)
}()
우리는 몇 초 후에 흥미로운 문제에 직면하게 될 것이다.
panic: too many concurrent operations on a single file or socket
이것은 말하지 않아도 알 수 있는 문제다.해결 방안은 사용 신호량이다.그러나 코드가 너무 복잡해져서 우리가 이루고 싶은 간단한 목표를 이룰 수 없을까 봐 걱정된다.
maxWorkers = runtime.GOMAXPROCS(0)
sem = semaphore.NewWeighted(int64(maxWorkers))

ctx := context.TODO()

for {
    if err := sem.Acquire(ctx, 4); err != nil {
    log.Printf("Failed to acquire semaphore: %v", err)
    break
    }

    go func() {
        defer sem.Release(2)
        yesChan <- bytes.Repeat([]byte(expletive), 100000000)
    }()

    go func() {
    defer sem.Release(2)
    os.Stdout.Write(<-yesChan)
    }()
}
그래서 이것은 틀림없이 폭탄일 거야, 그렇지?얼마나 빠른지 봅시다...
서스펜스
[1.42GiB/s]
왜?왜 그것은 실제로 이전의 직렬 작업에서 1억 번의 쓰기 작업과 같습니까?
답은 간단하다. 협동 루트가 병행 작업이라고 해도 I/O 작업이 아니기 때문에 개선은 거의 뚜렷하지 않다.동일한 출력을 동시에 두 번 쓸 수 없습니다.이것은 틀림없이 직렬 조작일 것이다. 이것이 바로 왜 지나치게 복잡한goroutines 코드가 여기에 도움이 되지 않는가이다.

실현 4: 바이트는 우리의 마지막 희망이다


지금까지 우리는 문자열을 사용해 왔다는 것을 주의하십시오.우리는 그것을 바이트로 바꾸었다
yesChan = make(chan []byte)
go func() {
    defer sem.Release(2)
    yesChan <- bytes.Repeat([]byte(expletive), 100000000)
}()
go func() {
    defer sem.Release(2)
    os.Stdout.Write(<-yesChan)
}()
우리는 무슨 개선이 있습니까?맞다(하지만 많지 않음) [1.63GiB/s] 따라서 문자열이 아닌 바이트를 사용하는 것이 도움이 된다.

구현 5: 기본으로 돌아가기


Goroutine/Semaphore 혼란이 필요하지 않기 때문에 이 스크립트를 기반으로 가져올 수 있습니다.
for {
    os.Stdout.Write(bytes.Repeat([]byte(expletive), 100000000))
}
이것이 바로 우리가 1.63GiB/s를 실현하는 데 필요한 전부입니다!
그 밖에 당신의 이정수를 고려하면 다를 수 있습니다. 왜냐하면 이 기능을 실행하는 컴퓨터도 일정한 역할을 하기 때문입니다.나는 다른 사람의 코드를 실행해 보았는데, 보도에 의하면 속도가 약 4GiB/s라고 하는데, 나는 절반밖에 얻지 못했기 때문에, 만약 당신이 당신의 기계에서 그것을 실행하고 싶다면, 고려해 보세요.

실현 6: 바이트 버퍼가 도움이 됩니까?


요컨대 적어도 나의 중등 지식으로 말하자면, 그것은 아닌 것 같다.
for {
    b.Write(bytes.Repeat([]byte(expletive), 100000000))
    b.WriteTo(os.Stdout)
}
즉, [1.28GiB/s]

실현 7: 적당한bufio 버퍼 어때요?


맞다이것은 내가 사용하는 프로세서와 메모리가 달성할 수 있는 가장 빠른 속도이다.
for {
    f := bufio.NewWriterSize(os.Stdout, 8192)
    f.Write(bytes.Repeat([]byte(expletive+"\n"), 100000000))
}
이것은 나에게 [1.73GiB/s]지금까지 나는 더 많은 것을 실현할 수 없었다. 만약 당신이 더 좋은 힌트를 가지고 있다면 나에게 알려주세요.

전체 버전 7 구현(최대)


이것은 완전한 코드다.대부분 명령줄 플래그와 관련된 템플릿 파일로 내용이 매우 적다.
package main

import (
    "bytes"
    "github.com/urfave/cli"
    "os"
    "bufio"
)

func yes() *cli.App {
    app := cli.NewApp()
    app.Name = "yes"
    app.Usage = "yes outputs expletive, or, by default, ``y'', forever"
    app.Action = func(c *cli.Context) error {
    expletive := c.Args().Get(0)
        if expletive == "" {
            expletive = "y"
        }

        for {
            f := bufio.NewWriterSize(os.Stdout, 8192)
            f.Write(bytes.Repeat([]byte(expletive+"\n"), 100000000))
        }
    }
    return app
}

func main() {
    app := yes()
    app.Run(os.Args)
}

한층 더 읽다

  • BSD 출처: https://github.com/openbsd/src/blob/master/usr.bin/yes/yes.c
  • GNU 출처: https://github.com/coreutils/coreutils/blob/master/src/yes.c
  • Reddit-GNU yes 왜 이렇게 빨라: https://www.reddit.com/r/unix/comments/6gxduc/how_is_gnu_yes_so_fast/
  • 좋은 웹페이지 즐겨찾기