-
Notifications
You must be signed in to change notification settings - Fork 364
feat: Add Korean translation for assembly lessons #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
notJoon
wants to merge
10
commits into
FFmpeg:main
Choose a base branch
from
notJoon:asm-lessons-ko
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
c7bd5fe
translate lesson 1 partially
notJoon 2216fe3
translate lesson 01 register parts
notJoon 83ff8e9
finishing lesson 01
notJoon 19757c2
translate lesson 2 partially
notJoon 33eff68
fix: typo
notJoon a4725a4
finishing lesson 2
notJoon b4b412c
fix: lesson 2
notJoon 7293200
fix: apply suggestions
notJoon e170de7
fix: apply suggestions and correct spelling
notJoon e641744
apply suggestions
notJoon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # FFmpeg 어셈블리어 강의 1강 | ||
|
|
||
| ## 소개 | ||
|
|
||
| FFmpeg 어셈블리어 학교에 오신걸 환영합니다. 여러분은 프로그래밍에서 가장 흥미로우면서 도전적이고 보람도 있는 여정의 첫걸음을 내디디셨습니다. 이 강의를 통해 여러분은 FFmpeg에서 어셈블리어 코드를 작성하는 방법의 기초를 배우고, 컴퓨터에서 실제로는 어떤 일이 일어나고 있는지에 눈뜨게 될 것입니다. | ||
|
|
||
| ## 사전 지식 | ||
|
|
||
| - C 언어 지식, 특히 포인터에 대한 이해가 필요합니다. 만약 아직 C에 익숙하지 않다면 [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) 책을 참고하세요. | ||
| - 고등학교 수학 (스칼라 vs 벡터, 덧셈, 곱셈 등) | ||
|
|
||
| ## 어셈블리어란 무엇일까요? | ||
|
|
||
| 어셈블리어는 CPU가 처리하는 명령어와 직접 대응하는 코드를 작성하는 프로그래밍 언어입니다. 사람이 읽을 수 있는 형태의 어셈블리어는 이름 그대로 '조립(assemble)'되어 CPU가 이해할 수 있는 이진 데이터, 즉 머신 코드(machine code)로 변환됩니다. 어셈블리어 코드는 종종 "어셈블리" 또는 "asm"으로 불립니다. | ||
|
|
||
| FFmpeg에서 사용되는 대부분의 어셈블리어 코드는 SIMD(단일 명령어 다중 데이터)로 작성되어 있습니다. SIMD는 종종 벡터 프로그래밍(vector programming)이라고도 불립니다. 이 방식은 하나의 명령어가 동시에 여러 데이터 요소를 처리할 수 있게 해줍니다. 반면 대부분의 프로그래밍 언어는 한 번에 하나의 데이터만 처리하는 스칼라 프로그래밍(scalar programming) 방식입니다. | ||
|
|
||
| 짐작하셨겠지만, SIMD는 이미지, 비디오, 오디오처럼 데이터의 대부분이 메모리에 연속적으로 정렬되어 있을 때 매우 효과적입니다. CPU에는 이렇게 연속적인 데이터를 처리하는 데 특화된 명령어들이 내장되어 있습니다. | ||
|
|
||
| FFmpeg에서는 "어셈블리 함수", "SIMD", "벡터화(vectorize)"라는 용어들이 혼용되지만, 모두 여러 데이터 요소를 한 번에 처리하기 위해 어셈블리어로 직접 함수를 작성했다는 의미입니다. 어떤 프로젝트에서는 이를 "어셈블리 커널(assembly kernel)"이라고 부르기도 합니다. | ||
|
|
||
| 이 모든 것이 당장은 복잡하게 느껴질 수도 있지만, FFmpeg에서는 고등학생들도 어셈블리 코드를 작성했다는 점을 기억하는 게 중요합니다. 모든 것이 그렇듯이 공부의 반은 용어이고 나머지 반이 진짜 공부입니다. | ||
|
|
||
| ## 왜 어셈블리어를 사용하나요? | ||
|
|
||
| 결론부터 말씀드리자면 멀티미디어를 빠르게 처리하기 위해서입니다. 어셈블리어로 코드를 작성해서 속도를 10배 이상 향상시키는 일은 아주 흔하며, 영상을 실시간으로 끊김 없이 재생하려면 특히 중요합니다. 추가로 에너지 소비를 줄이고 배터리 수명을 늘리는 데도 도움이 됩니다. 비디오 인코딩 및 디코딩 기능은 개인 사용자부터 대기업의 데이터센터에 이르기까지 지구상에서 가장 많이 사용되는 기능 중 하나라는 점도 중요합니다. 따라서 작은 개선도 빠르게 누적됩니다. | ||
|
|
||
| <!-- 문단의 어투나 내용을 봤을 때 online을 인터넷 커뮤니티로 번역하는게 문맥상 자연스러워 보임. --> | ||
| 인터넷 커뮤니티에서는 컴파일러 인트린직(intrinsic)을 사용하는걸 흔하게 볼 수 있습니다. 이는 어셈블리 명령어에 매핑되는 C 스타일 함수로, 주로 개발 속도를 높이기 위해 사용됩니다. 하지만 FFmpeg에서는 이러한 intrinsic을 사용하지 않고 어셈블리어 코드를 직접 작성합니다. 이 부분에 대해선 논쟁이 있지만, 컴파일러에 따라 일반적으로 intri은sics는 수작업으로 작성된 어셈블리 코드보다 약 10~15% 정도 느린 것으로 알려져 있습니다. FFmpeg에서는 가능한 모든 성능을 끌어내는 것이 중요하기 때문에, 인트린직에 의존하지 않는 방식을 택하고 있습니다 (옹호자들은 동의하지 않겠지만요). 또한 intrinsic은 [헝가리안 표기법](https://en.wikipedia.org/wiki/Hungarian_notation)을 사용하고 있기 때문에 읽기 어렵다는 의견도 있습니다. | ||
| 수 | ||
| 또한 FFmpeg 코드 중 일부에는 역사적인 이유로 인라인 어셈블리가 남아 있는 경우가 있으며, 리눅스 커널 같은 프로젝트에서도 아주 특수한 목적을 위해 사용되곤 합니다. 인라인 어셈블리는 어셈블리어를 별도의 파일이 아닌 C 코드 내부에 직접 작성하는 방식입니다. 하지만 이런 코드는 읽기 어려운데다, 여러 컴파일러에서 널리 지원하지 않으며, 유지보수가 어렵다는 것이 FFmpeg와 같은 프로젝트에서의 주된 의견입니다 | ||
|
|
||
| 마지막으로 인터넷에서는 "컴파일러가 알아서 벡터화를 해주기 때문에 어셈블리어는 더 이상 필요 없다"고 주장하는 '자칭 전문가'들도 자주 볼 수 있습니다. 하지만 적어도 학습 단계에서는 신경쓰지 않아도 됩니다. 예를 들어 [dav1d 프로젝트](https://www.videolan.org/projects/dav1d.html)의 최근 테스트에서는 컴파일러의 자동 벡터화가 약 2배의 속도 향상을 보여준 반면, 수작업으로 작성한 버전은 최대 8배까지의 성능 개선을 보여주었습니다. | ||
|
|
||
| ## 어셈블리어의 종류 | ||
|
|
||
| 이 강의는 x86 64비트 어셈블리어를 중심으로 다룹니다. amd64라고도 하지만 인텔 CPU에서도 동일하게 동작합니다. 이 밖에도 ARM이나 RISC-V 같은 다른 CPU용 어셈블리어가 있으며, 이러한 아키텍처 까지 강의가 확장될 가능성도 있습니다. | ||
|
|
||
| x86 어셈블리 문법은 크게 AT&T과 Intel 두 가지 형태가 있습니다. AT&T 문법은 Intel에 비해 오래되고 읽기가 어렵기 때문에 여기서는 Intel 문법을 사용하겠습니다. | ||
|
|
||
| ## 참고 자료 | ||
|
|
||
| 책이나 Stack Overflow 같은 온라인 자료가 참고 자료로서 특별히 유용하지는 않다고 하면 놀라실 것 같습니다. 이는 부분적으로 Intel 문법을 사용한 수작업 어셈블리를 선택했기 때문이기도 하지만, 많은 온라인 자료들이 주로 운영체제나 하드웨어 프로그래밍에 초점을 맞추고 있으며, 대부분 SIMD를 사용하지 않는 코드를 다루고 있기 때문입니다. FFmpeg 어셈블리는 특히 고성능 이미지 처리에 중점을 두고 있으며, 보시게 되겠지만 어셈블리 프로그래밍에서 특히 독특한 접근 방식을 취합니다. 하지만 이 강좌를 완료하고 나면 다른 어셈블리 활용 사례들을 쉽게 이해할 수 있게 될 것입니다. | ||
|
|
||
| 많은 책들이 어셈블리를 가르치기 전에 컴퓨터 아키텍처에 대한 세부 사항을 자세히 다룹니다. 그런 것을 배우고 싶다면 괜찮지만, 우리 관점에서는 자동차 운전을 배우기 전에 엔진을 공부하는 것과 같습니다. | ||
|
|
||
| 하지만 "The Art of 64-bit assembly" 책의 후반부에 나오는 다이어그램들은 도움이 됩니다. 이 다이어그램들은 SIMD 명령어들과 그 동작을 시각적으로 보여줍니다: https://artofasm.randallhyde.com/ | ||
|
|
||
| 질문을 할 수 있는 디스코드 서버도 있습니다: https://discord.com/invite/Ks5MhUhqfB | ||
|
|
||
| ## 레지스터(Register) | ||
|
|
||
| 레지스터는 CPU 내부에서 데이터를 처리하는 영역입니다. CPU는 메모리를 직접 연산하지 않고, 먼저 데이터를 레지스터로 불러온(load) 뒤 처리하고, 다시 메모리에 기록하는 방식으로 동작합니다. 어셈블리어에서는 일반적으로 한 메모리 위치에서 다른 메모리 위치로 데이터를 바로 복사할 수 없으며, 반드시 레지스터를 거쳐야 합니다. | ||
|
|
||
| ## 범용 레지스터(General Purpose Register) | ||
|
|
||
| 첫 번째 종류의 레지스터는 범용 레지스터(GPR, General Purpose Register)라고 불립니다. '범용'이라는 이름은 이 레지스터가 데이터(최대 64비트 값)나 메모리 주소(포인터)를 모두 담을 수 있기 때문에 붙었습니다. GPR에 들어 있는 값은 덧셈, 곱셈, 시프트(shift) 등의 연산을 통해 처리할 수 있습니다. | ||
|
|
||
| 대부분의 어셈블리 관련 서적에서는 GPR 사이의 차이점이나, GPR의 역사적 배경 등에 대해 지면을 할애해 다룹니다. 이는 운영체제 프로그래밍이나 리버스 엔지니어링처럼 GPR이 중요한 분야가 있기 때문입니다. FFmpeg에서 작성하는 어셈블리 코드에서 GPR은 형식적인 도구에 가깝고, 대부분의 경우 그런 복잡한 맥락은 불필요하기 때문에 추상화되어 사라집니다. | ||
|
|
||
| ## 벡터 레지스터(Vector Register) | ||
|
|
||
| 벡터(SIMD) 레지스터는 이름 그대로 여러 개의 데이터 요소를 담을 수 있는 레지스터입니다. 이 레지스터에는 여러 종류가 있습니다. | ||
|
|
||
| - **mm 레지스터**: MMX 레지스터, 64비트 크기, 현재는 잘 사용되지 않음 | ||
| - **xmm 레지스터**: XMM 레지스터, 128비트 크기, 대부분의 환경에서 사용 가능 | ||
| - **ymm 레지스터**: YMM 레지스터, 256비트 크기, 사용 시 몇 가지 제약이 있음 | ||
| - **zmm 레지스터**: ZMM 레지스터, 512비트 크기, 사용 가능 환경이 제한적임 | ||
|
|
||
| 비디오 압축과 압축 해제에서 대부분의 연산은 정수 기반이므로 여기서는 정수 데이터를 기준으로 설명하겠습니다. | ||
|
|
||
| 16개의 바이트가 xmm 레지스터에 담긴 예시입니다. | ||
|
|
||
| | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | | ||
| | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | ||
|
|
||
| 하지만 8개의 워드(16비트 정수)가 될 수도 있습니다. | ||
|
|
||
| | a | b | c | d | e | f | g | h | | ||
| | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | ||
|
|
||
| 또는 4개의 더블워드(32비트 정수)의 형태로도 | ||
|
|
||
| | a | b | c | d | | ||
| | :---- | :---- | :---- | :---- | | ||
|
|
||
| 아니면 2개의 쿼드워드(64비트 정수)가 될 수 있습니다. | ||
|
|
||
| | a | b | | ||
| | :---- | :---- | | ||
|
|
||
| 정리하면 다음과 같습니다. 굵게 표시한 글자는 이후에도 중요하게 사용됩니다. | ||
|
|
||
| - **b**ytes - 8비트 데이터 | ||
| - **w**ords - 16비트 데이터 | ||
| - **d**oublewords - 32비트 데이터 | ||
| - **q**uadwords - 64비트 데이터 | ||
| - **d**ouble **q**uadwords - 128비트 데이터 | ||
|
|
||
| ## x86inc.asm 포함 (include) | ||
|
|
||
| 많은 예제에서 x86inc.asm 파일을 포함하는 것을 볼 수 있습니다. x86inc.asm는 FFmpeg, x264, dav1d에서 사용하는 가벼운 추상화 레이어로, 어셈블리 프로그래머의 작업을 훨씬 수월하게 만들어 줍니다. 다양한 기능을 제공하지만, 우선 가장 유용한 기능 중 하나는 범용 레지스터(GPR)을 `r0`, `r1`, `r2` 처럼 라벨링해 준다는 점입니다. 덕분에 실제 레지스터 이름을 일일이 외울 필요가 없습니다. 앞서 언급했듯 GPR은 주로 보조 역할을 하기 때문에 이런 기능은 작업을 훨씬 간단하게 만들어 줍니다. | ||
| <!-- 원문에서는 scaffolding을 사용했지만 비계로 직역하면 문장이 어색해서 '보조'로 의역을 했습니다 --> | ||
|
|
||
| ## 간단한 어셈블리 예제 | ||
|
|
||
| 이번에는 인위적이지만 간단한 스칼라 어셈블리 코드를 보면서 어떤 일이 일어나는지 살펴보겠습니다. 여기서 _스칼라 어셈블리_ 란, 각 명령이 한 번에 하나의 데이터 항목만 처리하는 어셈블리 코드를 의미합니다. | ||
|
|
||
| ```assembly | ||
| mov r0q, 3 | ||
| inc r0q | ||
| dec r0q | ||
| imul r0q, 5 | ||
| ``` | ||
|
|
||
| 첫 번째 줄에서는 즉시값 `3`(메모리에서 읽어온 값이 아닌 코드 자체에 저장된 값)을 레지스터 `r0`에 쿼드워드(64비트) 크기로 저장합니다. | ||
|
|
||
| Intel 문법에서는 오른쪽에 있는 소스 피연산자가 왼쪽에 있는 목적지 피연산자로 전달됩니다. 소스 피연산자는 값이나 데이터가 있는 위치를 의미하고, 목적지 피연산자는 데이터를 받는 위치를 나타냅니다. 이 동작은 `memcpy`의 동작 방식과 유사합니다. 따라서 `r0q = 3`이라고 읽어도 의미가 같습니다. 여기서 `r0` 뒤에 붙은 `q` 접미사는 해당 레지스터를 쿼드워드 크기로 사용한다는 뜻입니다. | ||
|
|
||
| `inc` 명령은 값을 1 증가시켜 `r0q`를 4로 만들고, `dec` 명령은 값을 1 감소시켜 다시 3으로 되돌립니다. 마지막으로 `imul` 명령으로 5를 곱해 최종적으로 `r0q`에는 15가 저장됩니다. | ||
|
|
||
| 참고로 `mov`, `inc`와 같이 사람이 읽을 수 있는 형태의 명령어는 어셈블러에 의해 머신 코드로 변환되며, 이를 **니모닉(mnemonic)** 이라고 부릅니다. 책이나 온라인 자료에서는 니모닉을 `MOV`, `INC` 같이 대문자로 표기하는 경우도 있지만 의미는 동일합니다. FFmpeg에서는 니모닉은 소문자로 작성하고 대문자는 매크로 용도로만 사용합니다. | ||
|
|
||
| ## 기본 벡터 함수 이해하기 | ||
|
|
||
| 다음은 우리가 처음으로 살펴볼 SIMD 함수입니다. | ||
|
|
||
| ```assembly | ||
| %include "x86inc.asm" | ||
|
|
||
| SECTION .text | ||
|
|
||
| ;static void add_values(uint8_t *src, const uint8_t *src2) | ||
| INIT_XMM sse2 | ||
| cglobal add_values, 2, 2, 2, src, src2 | ||
| movu m0, [srcq] | ||
| movu m1, [src2q] | ||
|
|
||
| paddb m0, m1 | ||
|
|
||
| movu [srcq], m0 | ||
|
|
||
| RET | ||
| ``` | ||
|
|
||
| 한 줄씩 살펴보겠습니다. | ||
|
|
||
| ```assembly | ||
| %include "x86inc.asm" | ||
| ``` | ||
|
|
||
| 이 파일은 x264, FFmpeg, dav1d 커뮤니티에서 만든 "헤더(header)"로 코드 작성 시 유용한 헬퍼, 미리 정의한 이름, 매크로(cglobal 등)를 제공해 코드를 쉽게 작성할 수 있도록 해줍니다. | ||
|
|
||
| ```assembly | ||
| SECTION .text | ||
| ``` | ||
|
|
||
| 여기서는 실행할 코드가 위치하는 섹션을 지정합니다. 상수 데이터를 넣는 `.data` 섹션과는 대비됩니다. | ||
|
|
||
| ```assembly | ||
| ;static void add_values(uint8_t *src, const uint8_t *src2) | ||
| INIT_XMM sse2 | ||
| ``` | ||
|
|
||
| 첫 번째 줄은 주석이며 C 코드에서 해당 함수의 인자 형식을 보여줍니다. 어셈블리에서 세미콜론 `;`은 C의 `//`와 동일한 역할을 합니다. | ||
|
|
||
| 두 번째 줄은 SSE2 명령어 집합을 이용해 해당 함수가 XMM 레지스터를 사용하도록 설정합니다. 이는 아래에서 사용할 `paddb`가 SSE2 명령어이기 때문입니다. SSE2에 대해서는 다음 강의에서 더 자세히 다룰 예정입니다. | ||
|
|
||
| ```assembly | ||
| cglobal add_values, 2, 2, 2, src, src2 | ||
| ``` | ||
|
|
||
| 이 줄은 `add_values`라는 C 함수를 정의합니다. 각 인자의 의미를 살펴보면, | ||
|
|
||
| - 첫 번째 인자(2)는 함수 인자가 두 개라는 뜻입니다. | ||
| - 두 번째 인자(2)는 인자를 전달하기 위해 두 개의 GPR을 사용할 것임을 나타냅니다. 더 많은 GPR이 필요하다면 여기서 개수를 늘려야 합니다. | ||
| - 세 번째 인자(2)는 사용할 XMM 레지스터의 개수를 나타냅니다. | ||
| - 마지막 두 인자(`src`, `src2`)는 함수 인자의 레이블(label)입니다. | ||
|
|
||
| 참고로 이전 코드에서는 인자 레이블 없이 `r0`, `r1`처럼 GPR을 직접 참조하는 방식이 사용되기도 했습니다. | ||
|
|
||
| ```assembly | ||
| movu m0, [srcq] | ||
| movu m1, [src2q] | ||
| ``` | ||
|
|
||
| `movu`는 `movdqu`(move double quad unaligned)의 약자입니다. 정렬(alignment)에 대해서는 이후 강의에서 다루지만, 일단 `movu`를 `[srcq]`에서 128비트를 읽어오는 명령으로 이해하면 됩니다. | ||
|
|
||
| `mov`에서 대괄호 `[]`는 해당 주소를 역참조한다는 뜻이며, C언어의 `*src`와 같습니다. 이를 **로드(load)** 라고 합니다. 여기서 `q` 접미사는 포인터 크기를 나타내며 (64비트 시스템에서 `sizeof(*src) == 8`), 32비트 시스템에서는 자동으로 32비트를 사용합니다. 하지만 실제 로드는 128비트 단위로 이루어집니다. | ||
|
|
||
| 또한 벡터 레지스터는 `xmm0`같은 전체 이름이 아닌 추상화된 `m0`으로 부릅니다. 이렇게 하면 한 번 작성한 코드를 다양한 크기의 SIMD 레지스터에서 사용할 수 있습니다. | ||
|
|
||
| ```assembl | ||
| paddb m0, m1 | ||
| ``` | ||
|
|
||
| `paddb`(머릿속에서 *p-add-b*라고 읽으면 됩니다)는 각 레지스터의 각 바이트를 서로 더하는 명령입니다. 여기서 접두사 `p`는 *packed*를 의미하며, 벡터 명령어와 스칼라 명령어를 구분하는데 쓰입니다. 접미사 `b`는 이 연산이 바이트 단위 덧셈임을 나타냅니다. | ||
|
|
||
| | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | | ||
| | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | ||
|
|
||
| \+ | ||
|
|
||
| | q | r | s | t | u | v | w | x | y | z | aa | ab | ac | ad | ae | af | | ||
| | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | ||
|
|
||
| \= | ||
|
|
||
| | a+q | b+r | c+s | d+t | e+u | f+v | g+w | h+x | i+y | j+z | k+aa | l+ab | m+ac | n+ad | o+ae | p+af | | ||
| | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | ||
|
|
||
| ```assembly | ||
| movu [srcq], m0 | ||
| ``` | ||
|
|
||
| 이 명령은 **스토어(store)** 라고 불립니다. `scrq` 포인터가 가리키는 주소에 데이터를 다시 써 넣는 동작입니다. | ||
|
|
||
| ```assembly | ||
| RET | ||
| ``` | ||
|
|
||
| 이 매크로는 함수가 반환됨을 나타냅니다. FFmpeg의 거의 모든 어셈블리 함수는 값을 반환하기보다는 인자로 전달된 데이터 자체를 수정하는 방식을 사용합니다. | ||
|
|
||
| 곧 보게 될 실습에서는 어셈블리 함수를 가리키는 함수 포인터를 만들고 활용하는 시간을 가지겠습니다. | ||
|
|
||
| [다음 강의](../lesson_02/index.ko.md) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor typo:
scrq->srcq