[{"data":1,"prerenderedAt":1727},["ShallowReactive",2],{"blog-ko-why-gpdf-is-faster":3},{"id":4,"title":5,"author":6,"body":9,"date":1713,"description":1714,"draft":1715,"extension":1716,"howTo":1717,"image":1717,"meta":1718,"navigation":968,"path":1719,"seo":1720,"stem":1721,"tags":1722,"updated":1717,"__hash__":1726},"blogKo/ko/blog/011.why-gpdf-is-faster.md","gpdf가 다른 Go PDF 라이브러리보다 10–30배 빠른 이유",{"name":7,"url":8},"gpdf team","https://gpdf.dev",{"type":10,"value":11,"toc":1692},"minimark",[12,17,43,69,72,85,92,96,103,219,229,236,243,247,250,260,271,278,285,618,654,677,680,685,688,707,718,729,732,736,739,746,843,873,876,879,1032,1041,1051,1055,1062,1068,1078,1081,1087,1090,1094,1108,1111,1115,1118,1125,1128,1158,1169,1172,1179,1310,1313,1317,1331,1334,1338,1352,1359,1363,1366,1444,1451,1454,1458,1461,1471,1481,1495,1503,1506,1510,1513,1556,1565,1568,1580,1584,1595,1601,1614,1620,1626,1630,1633,1648,1661,1665,1688],[13,14,16],"h2",{"id":15},"tldr","TL;DR",[18,19,20,21,25,26,29,30,33,34,38,39,42],"p",{},"gpdf는 단일 페이지를 ",[22,23,24],"strong",{},"13 µs",", 4×10 인보이스 표를 ",[22,27,28],{},"108 µs",", 100페이지 분할 보고서를 ",[22,31,32],{},"683 µs","에 생성한다. 그다음으로 빠른 유지보수 중인 Go PDF 라이브러리 ",[35,36,37],"code",{},"jung-kurt/gofpdf","는 같은 100페이지를 ",[22,40,41],{},"11.7 ms","에 처리 — 약 17배 느리다. 튜닝 차이가 아니다. 세 가지 설계 결정이 쌓인 결과다.",[44,45,46,53,63],"ol",{},[47,48,49,52],"li",{},[22,50,51],{},"단일 패스 레이아웃."," Builder API와 PDF 콘텐츠 스트림 사이에 중간 AST가 없다.",[47,54,55,58,59,62],{},[22,56,57],{},"핫패스의 구체 타입."," 레이아웃 루프 안에 리플렉션, ",[35,60,61],{},"interface{}",", 가상 디스패치 없음.",[47,64,65,68],{},[22,66,67],{},"cmap을 한 번만 해석하는 TrueType 서브세터."," 글리프마다도, 페이지마다도 아닌 단 한 번.",[18,70,71],{},"각각 하나씩이 2–3배. 셋을 합하면 한 자릿수 차이가 된다.",[18,73,74,75,84],{},"이 글은 그 숫자를 만든 코드 경로를 그대로 따라간다. 벤치마크 소스는 ",[76,77,81],"a",{"href":78,"rel":79},"https://github.com/gpdf-dev/gpdf/tree/main/_benchmark",[80],"nofollow",[35,82,83],{},"_benchmark/benchmark_test.go","에 공개되어 있다. 클론해서 자신의 머신에서 돌려보고, 숫자가 맞지 않으면 이슈를 열어 주면 된다.",[18,86,87,88,91],{},"선공개 바이어스: 우리는 gpdf를 만드는 팀이다. \"우리 게 더 빠르다\"의 정직한 번역은 \"우리는 다른 트레이드오프를 선택했다\"이고, 흥미로운 질문은 ",[22,89,90],{},"그 속도를 위해 무엇을 포기했나","다. 후반부에서 그 이야기를 한다.",[13,93,95],{"id":94},"여기서-빠르다는-무엇인가","여기서 \"빠르다\"는 무엇인가",[18,97,98,99,102],{},"아키텍처 전에, 설명하려는 스코어보드를 먼저 (Apple M1, Go 1.25, CGO 비활성, ",[35,100,101],{},"-benchmem"," 활성):",[104,105,106,131],"table",{},[107,108,109],"thead",{},[110,111,112,116,119,122,125,128],"tr",{},[113,114,115],"th",{},"워크로드",[113,117,118],{},"gpdf",[113,120,121],{},"gofpdf",[113,123,124],{},"go-pdf/fpdf",[113,126,127],{},"signintech/gopdf",[113,129,130],{},"Maroto v2",[132,133,134,156,177,197],"tbody",{},[110,135,136,140,144,147,150,153],{},[137,138,139],"td",{},"단일 페이지 Hello World",[137,141,142],{},[22,143,24],{},[137,145,146],{},"132 µs",[137,148,149],{},"135 µs",[137,151,152],{},"423 µs",[137,154,155],{},"237 µs",[110,157,158,161,165,168,171,174],{},[137,159,160],{},"4×10 인보이스 표",[137,162,163],{},[22,164,28],{},[137,166,167],{},"241 µs",[137,169,170],{},"243 µs",[137,172,173],{},"835 µs",[137,175,176],{},"8,600 µs",[110,178,179,182,186,189,192,194],{},[137,180,181],{},"100페이지 분할 보고서",[137,183,184],{},[22,185,32],{},[137,187,188],{},"11,700 µs",[137,190,191],{},"11,900 µs",[137,193,176],{},[137,195,196],{},"19,800 µs",[110,198,199,202,207,210,213,216],{},[137,200,201],{},"복잡한 CJK 인보이스",[137,203,204],{},[22,205,206],{},"133 µs",[137,208,209],{},"254 µs",[137,211,212],{},"n/a",[137,214,215],{},"997 µs",[137,217,218],{},"10,400 µs",[18,220,221,222,225,226,228],{},"설명을 시작하기 전에 보이는 두 가지 모양이 있다. 페이지 수가 늘수록 ",[22,223,224],{},"격차가 벌어진다"," (Hello World 10배, 100페이지 17배). 복잡도가 올라갈수록 ",[22,227,224],{}," (표 단독으로 108 µs인데 Maroto의 gofpdf 백엔드로는 8.6 ms).",[18,230,231,232,235],{},"두 모양 모두 원인은 같다. ",[22,233,234],{},"gpdf의 레이아웃 루프는 공통 경로에서 할당하지 않기 때문에"," 요소당 비용이 거의 평탄하다. 왜 그런지 아래에서 설명한다.",[18,237,238,239,242],{},"아무도 읽고 싶어 하지 않는 면책 조항: ",[22,240,241],{},"대부분의 PDF 워크로드에서 절대 속도는 생각만큼 중요하지 않다",". 가장 큰 문서가 한 페이지짜리 영수증이라면 이 표의 유지보수 중인 어떤 라이브러리든 요청 경로에서 생성 가능하다. 격차가 의미를 갖는 임계점은 \"100개 인보이스를 큐로 미루지 않고 동기 생성 가능한가\" 근처부터다.",[13,244,246],{"id":245},"결정-1-중간-ast를-만들지-않는다","결정 1: 중간 AST를 만들지 않는다",[18,248,249],{},"일반적인 PDF Builder 라이브러리는 이렇게 동작한다:",[251,252,257],"pre",{"className":253,"code":255,"language":256},[254],"language-text","builder API → 문서 트리 (AST) → 레이아웃 패스 → 직렬화기 → 바이트\n","text",[35,258,255],{"__ignoreMap":259},"",[18,261,262,263,266,267,270],{},"가운데의 문서 트리 단계가 문제다. 매 ",[35,264,265],{},".Text()","마다 노드를 할당하고, 매 ",[35,268,269],{},".Row()","마다 컨테이너를 할당한다. 레이아웃 패스는 트리를 돌아 위치를 계산하고, 직렬화기는 다시 트리를 돌아 바이트를 낸다. 세 번의 패스, 세 번의 할당, 동일 데이터에 대한 세 번의 CPU 캐시 왕복.",[18,272,273,274,277],{},"gpdf에는 그 2단계가 없다. Builder는 레이아웃 컨텍스트에 직접 쓰고, 레이아웃 컨텍스트는 콘텐츠 스트림에 직접 쓴다. ",[22,275,276],{},"한 번의 패스",".",[18,279,280,281,284],{},"텍스트 요소의 실제 코드 경로 (",[35,282,283],{},"template/col_builder.go","에서 길이 조정):",[251,286,290],{"className":287,"code":288,"language":289,"meta":259,"style":259},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func (c *ColBuilder) Text(s string, opts ...TextOption) {\n    opt := c.resolveOptions(opts)\n    box := c.currentBox()\n    w := c.measureText(s, opt)\n    h := opt.FontSize.Pt() * opt.LineHeight\n    c.writer.BeginText()\n    c.writer.SetFont(opt.Font, opt.FontSize)\n    c.writer.MoveTo(box.X, box.Y-opt.FontSize.Pt())\n    c.writer.ShowString(s)\n    c.writer.EndText()\n    c.advance(w, h)\n}\n","go",[35,291,292,349,375,393,419,452,470,504,554,574,590,612],{"__ignoreMap":259},[293,294,297,301,304,308,311,315,318,322,325,328,332,335,338,341,344,346],"span",{"class":295,"line":296},"line",1,[293,298,300],{"class":299},"sMK4o","func",[293,302,303],{"class":299}," (",[293,305,307],{"class":306},"sHdIc","c ",[293,309,310],{"class":299},"*",[293,312,314],{"class":313},"sBMFI","ColBuilder",[293,316,317],{"class":299},")",[293,319,321],{"class":320},"s2Zo4"," Text",[293,323,324],{"class":299},"(",[293,326,327],{"class":306},"s",[293,329,331],{"class":330},"spNyl"," string",[293,333,334],{"class":299},",",[293,336,337],{"class":306}," opts",[293,339,340],{"class":299}," ...",[293,342,343],{"class":313},"TextOption",[293,345,317],{"class":299},[293,347,348],{"class":299}," {\n",[293,350,352,356,359,362,364,367,369,372],{"class":295,"line":351},2,[293,353,355],{"class":354},"sTEyZ","    opt ",[293,357,358],{"class":299},":=",[293,360,361],{"class":354}," c",[293,363,277],{"class":299},[293,365,366],{"class":320},"resolveOptions",[293,368,324],{"class":299},[293,370,371],{"class":354},"opts",[293,373,374],{"class":299},")\n",[293,376,378,381,383,385,387,390],{"class":295,"line":377},3,[293,379,380],{"class":354},"    box ",[293,382,358],{"class":299},[293,384,361],{"class":354},[293,386,277],{"class":299},[293,388,389],{"class":320},"currentBox",[293,391,392],{"class":299},"()\n",[293,394,396,399,401,403,405,408,410,412,414,417],{"class":295,"line":395},4,[293,397,398],{"class":354},"    w ",[293,400,358],{"class":299},[293,402,361],{"class":354},[293,404,277],{"class":299},[293,406,407],{"class":320},"measureText",[293,409,324],{"class":299},[293,411,327],{"class":354},[293,413,334],{"class":299},[293,415,416],{"class":354}," opt",[293,418,374],{"class":299},[293,420,422,425,427,429,431,434,436,439,442,445,447,449],{"class":295,"line":421},5,[293,423,424],{"class":354},"    h ",[293,426,358],{"class":299},[293,428,416],{"class":354},[293,430,277],{"class":299},[293,432,433],{"class":354},"FontSize",[293,435,277],{"class":299},[293,437,438],{"class":320},"Pt",[293,440,441],{"class":299},"()",[293,443,444],{"class":299}," *",[293,446,416],{"class":354},[293,448,277],{"class":299},[293,450,451],{"class":354},"LineHeight\n",[293,453,455,458,460,463,465,468],{"class":295,"line":454},6,[293,456,457],{"class":354},"    c",[293,459,277],{"class":299},[293,461,462],{"class":354},"writer",[293,464,277],{"class":299},[293,466,467],{"class":320},"BeginText",[293,469,392],{"class":299},[293,471,473,475,477,479,481,484,486,489,491,494,496,498,500,502],{"class":295,"line":472},7,[293,474,457],{"class":354},[293,476,277],{"class":299},[293,478,462],{"class":354},[293,480,277],{"class":299},[293,482,483],{"class":320},"SetFont",[293,485,324],{"class":299},[293,487,488],{"class":354},"opt",[293,490,277],{"class":299},[293,492,493],{"class":354},"Font",[293,495,334],{"class":299},[293,497,416],{"class":354},[293,499,277],{"class":299},[293,501,433],{"class":354},[293,503,374],{"class":299},[293,505,507,509,511,513,515,518,520,523,525,528,530,533,535,538,541,543,545,547,549,551],{"class":295,"line":506},8,[293,508,457],{"class":354},[293,510,277],{"class":299},[293,512,462],{"class":354},[293,514,277],{"class":299},[293,516,517],{"class":320},"MoveTo",[293,519,324],{"class":299},[293,521,522],{"class":354},"box",[293,524,277],{"class":299},[293,526,527],{"class":354},"X",[293,529,334],{"class":299},[293,531,532],{"class":354}," box",[293,534,277],{"class":299},[293,536,537],{"class":354},"Y",[293,539,540],{"class":299},"-",[293,542,488],{"class":354},[293,544,277],{"class":299},[293,546,433],{"class":354},[293,548,277],{"class":299},[293,550,438],{"class":320},[293,552,553],{"class":299},"())\n",[293,555,557,559,561,563,565,568,570,572],{"class":295,"line":556},9,[293,558,457],{"class":354},[293,560,277],{"class":299},[293,562,462],{"class":354},[293,564,277],{"class":299},[293,566,567],{"class":320},"ShowString",[293,569,324],{"class":299},[293,571,327],{"class":354},[293,573,374],{"class":299},[293,575,577,579,581,583,585,588],{"class":295,"line":576},10,[293,578,457],{"class":354},[293,580,277],{"class":299},[293,582,462],{"class":354},[293,584,277],{"class":299},[293,586,587],{"class":320},"EndText",[293,589,392],{"class":299},[293,591,593,595,597,600,602,605,607,610],{"class":295,"line":592},11,[293,594,457],{"class":354},[293,596,277],{"class":299},[293,598,599],{"class":320},"advance",[293,601,324],{"class":299},[293,603,604],{"class":354},"w",[293,606,334],{"class":299},[293,608,609],{"class":354}," h",[293,611,374],{"class":299},[293,613,615],{"class":295,"line":614},12,[293,616,617],{"class":299},"}\n",[18,619,620,621,624,625,628,629,632,633,635,636,635,638,640,641,635,644,635,647,635,650,653],{},"노드가 트리에 쌓이지 않는다. 위치가 지연되지 않는다. writer는 ",[35,622,623],{},"io.Writer","(보통 ",[35,626,627],{},"bytes.Buffer",")를 가진 ",[35,630,631],{},"*pdf.Writer","이고, ",[35,634,467],{}," / ",[35,637,517],{},[35,639,567],{},"은 ",[35,642,643],{},"BT",[35,645,646],{},"Td",[35,648,649],{},"Tj",[35,651,652],{},"ET"," PDF 연산자를 그 버퍼에 즉시 쓴다.",[18,655,656,657,660,661,664,665,668,669,672,673,676],{},"gofpdf가 같은 논리 연산을 어떻게 하는지 비교. gofpdf는 ",[35,658,659],{},"page"," 객체에 연산의 슬라이스를 가진다. 각 ",[35,662,663],{},"SetXY"," + ",[35,666,667],{},"Cell"," 호출이 그 슬라이스에 append. 마지막에 ",[35,670,671],{},"Output"," (또는 ",[35,674,675],{},"OutputFileAndClose",")이 슬라이스를 돌며 바이트를 낸다. 셀당 할당 두 번 — 연산 구조체 하나, 문자열 복사 하나 — 과 데이터에 대한 추가 패스 하나.",[18,678,679],{},"100페이지 보고서에 페이지당 약 40줄이면, gpdf가 하지 않는 추가 할당이 4,000개다.",[681,682,684],"h3",{"id":683},"단일-패스가-아픈-곳","단일 패스가 아픈 곳",[18,686,687],{},"당연한 질문: 바이트 출력을 시작하기 전에 최종 페이지 레이아웃을 알아야 하는 건 어떻게 하나. 페이지 번호 넣은 헤더. 페이지를 넘나드는 표. 본문 마지막 줄 아래에 고정된 푸터.",[18,689,690,691,694,695,698,699,702,703,706],{},"두 가지 답. 첫째, 버퍼링의 단위는 ",[22,692,693],{},"문서가 아니라 페이지","다. 페이지는 수십 KB 단위의 경계 단위다. 다음 ",[35,696,697],{},"AddPage()","가 실행되면 현재 페이지 콘텐츠 스트림이 확정되고(",[35,700,701],{},"Length",", ",[35,704,705],{},"Filter",", 오프셋) xref 엔트리가 쓰이고, 페이지 버퍼는 리셋된다. 메모리 최고 수위는 O(페이지 하나) 크기로 유지된다.",[18,708,709,710,713,714,717],{},"둘째, 진짜 전역 요소(\"Page 3 of 27\")에 대해서는 ",[22,711,712],{},"그 범위만"," fix-up 패스로 지연한다. 나머지 내용은 이미 스트림에 있다. fix-up은 짧은 deferred-reference 마커 리스트를 돌며 패치한다. 코드베이스에서 AST 비용에 근접한 비용을 내는 유일한 지점이고, ",[22,715,716],{},"실제로 필요한 부분에만"," 낸다.",[18,719,720,721,724,725,728],{},"포기한 것: 노드 트리에 대한 임의의 후처리가 불가능하다. 노드 트리가 없기 때문이다. \"",[35,722,723],{},"bold: true","인 ",[35,726,727],{},"Text"," 노드 전부 재정렬\" 같은 플러그인은 쓸 수 없다. 그런 모양의 API가 필요하면 Maroto v2를 쓰면 된다.",[18,730,731],{},"gpdf가 타깃으로 하는 용도에는 이 트레이드오프가 옳다고 본다. PDF 대부분은 왼쪽에서 오른쪽으로, 위에서 아래로, 구성 시점에 알려진 레이아웃으로 생성된다. 소수 사례를 위해 AST를 유지하는 비용을 다수가 모든 페이지에서 지불해 왔다. 그 비율을 뒤집었다.",[13,733,735],{"id":734},"결정-2-핫패스에-리플렉션과-interface를-두지-않는다","결정 2: 핫패스에 리플렉션과 interface를 두지 않는다",[18,737,738],{},"이야기로는 덜 흥미롭지만 프로파일로 보면 남은 속도 차의 절반이 여기서 나온다.",[18,740,741,742,745],{},"gofpdf의 ",[35,743,744],{},"CellFormat"," 시그니처:",[251,747,749],{"className":287,"code":748,"language":289,"meta":259,"style":259},"func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string,\n    ln int, alignStr string, fill bool, link int, linkStr string) { ... }\n",[35,750,751,796],{"__ignoreMap":259},[293,752,753,755,757,760,762,765,767,770,772,774,776,778,781,783,786,788,791,793],{"class":295,"line":296},[293,754,300],{"class":299},[293,756,303],{"class":299},[293,758,759],{"class":306},"f ",[293,761,310],{"class":299},[293,763,764],{"class":313},"Fpdf",[293,766,317],{"class":299},[293,768,769],{"class":320}," CellFormat",[293,771,324],{"class":299},[293,773,604],{"class":306},[293,775,334],{"class":299},[293,777,609],{"class":306},[293,779,780],{"class":330}," float64",[293,782,334],{"class":299},[293,784,785],{"class":306}," txtStr",[293,787,334],{"class":299},[293,789,790],{"class":306}," borderStr",[293,792,331],{"class":330},[293,794,795],{"class":299},",\n",[293,797,798,801,804,806,809,811,813,816,819,821,824,826,828,831,833,835,838,840],{"class":295,"line":351},[293,799,800],{"class":306},"    ln",[293,802,803],{"class":330}," int",[293,805,334],{"class":299},[293,807,808],{"class":306}," alignStr",[293,810,331],{"class":330},[293,812,334],{"class":299},[293,814,815],{"class":306}," fill",[293,817,818],{"class":330}," bool",[293,820,334],{"class":299},[293,822,823],{"class":306}," link",[293,825,803],{"class":330},[293,827,334],{"class":299},[293,829,830],{"class":306}," linkStr",[293,832,331],{"class":330},[293,834,317],{"class":299},[293,836,837],{"class":299}," {",[293,839,340],{"class":299},[293,841,842],{"class":299}," }\n",[18,844,845,846,849,850,853,854,857,858,861,862,864,865,868,869,872],{},"문제없다. Maroto의 컴포넌트 트리를 보자. ",[35,847,848],{},"Row","는 ",[35,851,852],{},"[]Component","를 가진다. ",[35,855,856],{},"Component","는 interface다. 레이아웃 연산마다 가상 디스패치: ",[35,859,860],{},"component.Render(ctx)",". ",[35,863,727],{},"와 ",[35,866,867],{},"Spacer","가 든 하나의 ",[35,870,871],{},"Col","이면 세 번의 디스패치. 100페이지 × 페이지당 30줄 × 3개 컴포넌트면 9,000번.",[18,874,875],{},"Go interface 디스패치는 회당 2–3 ns 정도. 단독으로 죄는 아니다. 하지만 interface는 컴파일러가 박싱된 값을 힙에 두도록 강제한다 — Go 컴파일러가 항상 해 주지 않는 devirtualization 없이는 interface 너머로 스택 할당이 안 된다. 비용은 디스패치 자체가 아니라 그걸 먹이는 할당이다.",[18,877,878],{},"gpdf의 레이아웃 엔진은 구체 구조체를 쓴다:",[251,880,882],{"className":287,"code":881,"language":289,"meta":259,"style":259},"type RowBuilder struct {\n    doc    *Document\n    parent *pageState\n    spans  [12]int\n    cols   [12]ColBuilder  // 값 배열, 포인터도 interface도 아님\n    n      uint8\n}\n\ntype ColBuilder struct {\n    row    *RowBuilder\n    span   int\n    cursor document.Point\n    writer *pdf.Writer\n}\n",[35,883,884,897,907,917,935,952,960,964,970,981,991,998,1011,1027],{"__ignoreMap":259},[293,885,886,889,892,895],{"class":295,"line":296},[293,887,888],{"class":299},"type",[293,890,891],{"class":313}," RowBuilder",[293,893,894],{"class":299}," struct",[293,896,348],{"class":299},[293,898,899,902,904],{"class":295,"line":351},[293,900,901],{"class":354},"    doc    ",[293,903,310],{"class":299},[293,905,906],{"class":313},"Document\n",[293,908,909,912,914],{"class":295,"line":377},[293,910,911],{"class":354},"    parent ",[293,913,310],{"class":299},[293,915,916],{"class":313},"pageState\n",[293,918,919,922,925,929,932],{"class":295,"line":395},[293,920,921],{"class":354},"    spans  ",[293,923,924],{"class":299},"[",[293,926,928],{"class":927},"sbssI","12",[293,930,931],{"class":299},"]",[293,933,934],{"class":330},"int\n",[293,936,937,940,942,944,946,948],{"class":295,"line":421},[293,938,939],{"class":354},"    cols   ",[293,941,924],{"class":299},[293,943,928],{"class":927},[293,945,931],{"class":299},[293,947,314],{"class":313},[293,949,951],{"class":950},"sHwdD","  // 값 배열, 포인터도 interface도 아님\n",[293,953,954,957],{"class":295,"line":454},[293,955,956],{"class":354},"    n      ",[293,958,959],{"class":330},"uint8\n",[293,961,962],{"class":295,"line":472},[293,963,617],{"class":299},[293,965,966],{"class":295,"line":506},[293,967,969],{"emptyLinePlaceholder":968},true,"\n",[293,971,972,974,977,979],{"class":295,"line":556},[293,973,888],{"class":299},[293,975,976],{"class":313}," ColBuilder",[293,978,894],{"class":299},[293,980,348],{"class":299},[293,982,983,986,988],{"class":295,"line":576},[293,984,985],{"class":354},"    row    ",[293,987,310],{"class":299},[293,989,990],{"class":313},"RowBuilder\n",[293,992,993,996],{"class":295,"line":592},[293,994,995],{"class":354},"    span   ",[293,997,934],{"class":330},[293,999,1000,1003,1006,1008],{"class":295,"line":614},[293,1001,1002],{"class":354},"    cursor ",[293,1004,1005],{"class":313},"document",[293,1007,277],{"class":299},[293,1009,1010],{"class":313},"Point\n",[293,1012,1014,1017,1019,1022,1024],{"class":295,"line":1013},13,[293,1015,1016],{"class":354},"    writer ",[293,1018,310],{"class":299},[293,1020,1021],{"class":313},"pdf",[293,1023,277],{"class":299},[293,1025,1026],{"class":313},"Writer\n",[293,1028,1030],{"class":295,"line":1029},14,[293,1031,617],{"class":299},[18,1033,1034,1037,1038,277],{},[35,1035,1036],{},"cols","는 그리드 최대 열 수(12)에 맞춘 값 배열. 힙 할당 없음. 행이 컬럼을 순회할 때 interface 디스패치 없음. Builder가 writer의 포인터를 가지고, ",[22,1039,1040],{},"writer는 Builder 트리의 존재를 모른다",[18,1042,1043,1044,1047,1048,1050],{},"콜백 패턴 (",[35,1045,1046],{},"r.Col(4, func(c *ColBuilder) { ... })",")은 우연이 아니다. 프로토타입한 다른 모양 — 체이닝 가능한 struct 반환 API, Component interface의 박싱 트리 — 전부 느렸다. 이 클로저가 제로 할당인 이유는 ",[35,1049,314],{},"가 호출자가 포인터 매개변수로 가진 값이고, 클로저 자체가 대부분 escape analysis로 스택에 올라가기 때문이다.",[681,1052,1054],{"id":1053},"효과를-확인한-방법","효과를 확인한 방법",[18,1056,1057,1058,1061],{},"gpdf에서 ",[35,1059,1060],{},"go test -run=XXX -bench=BenchmarkSinglePage -memprofile=mem.out",":",[251,1063,1066],{"className":1064,"code":1065,"language":256},[254],"BenchmarkSinglePage-8   91270   13120 ns/op   8321 B/op   52 allocs/op\n",[35,1067,1065],{"__ignoreMap":259},[18,1069,1070,1071,1074,1075,1077],{},"전체 PDF 페이지 하나에 ",[22,1072,1073],{},"52회 할당",". 거의 전부가 초기 페이지 버퍼, 폰트 메트릭스 조회(폰트당 한 번, 글리프당이 아님), 마지막 ",[35,1076,627],{}," 확장. 레이아웃 루프는 제로 할당 — 프로파일을 보면 보인다.",[18,1079,1080],{},"같은 페이지의 gofpdf:",[251,1082,1085],{"className":1083,"code":1084,"language":256},[254],"BenchmarkGofpdfSinglePage-8   7500   132400 ns/op   71200 B/op   430 allocs/op\n",[35,1086,1084],{"__ignoreMap":259},[18,1088,1089],{},"430회 할당. 대부분 연산 슬라이스와 그걸 채우는 문자열 복사. 할당 약 8배 차이가 GC를 거쳐 실행 시간 약 10배 차이로 이어지는 건 자연스러운 귀결.",[681,1091,1093],{"id":1092},"포기한-것","포기한 것",[18,1095,1096,1097,1100,1101,1103,1104,1107],{},"핫패스 에르고노믹스 제로는 곧 ",[22,1098,1099],{},"확장 지점이 적다","는 뜻이다. gpdf 레이아웃에 플러그인되는 커스텀 요소 타입 — Maroto에서 ",[35,1102,856],{},"를 구현하는 것과 같은 일 — 은 쓸 수 없다. 만족시킬 interface가 없다. 대신 제공하는 것은 ",[35,1105,1106],{},"template.WithWriterSetup()","이다. PDF writer에 대한 훅으로 커스텀 어노테이션, PDF/A 메타데이터, 암호화 등을 주입할 수 있다. 레이아웃 레벨 확장은 사용자가 호출하는 것과 동일한 Builder 메서드를 호출하는 헬퍼 함수로 작성한다.",[18,1109,1110],{},"확장 지점이 적은 것은 진짜 비용이다. 현재 판단에서는 균형이 맞는다. 프로젝트 방향이 바뀌어 그 판단이 성립하지 않으면 재검토한다.",[13,1112,1114],{"id":1113},"결정-3-재주행하지-않는-truetype-서브세터","결정 3: 재주행하지 않는 TrueType 서브세터",[18,1116,1117],{},"CJK 벤치마크(gpdf 133 µs 대 gofpdf 254 µs)의 격차 대부분이 여기서 나온다.",[18,1119,1120,1121,1124],{},"TrueType 서브세팅이 하는 일 요약. PDF에 일본어 폰트를 임베드할 때 20,000+ 글리프 전부를 넣고 싶지 않다 — 100 KB 문서에 15 MB 폰트 데이터다. ",[22,1122,1123],{},"문서가 실제로 쓰는 글리프만"," PDF 리더가 디코드할 수 있는 유효한 서브셋 TTF로 패키징하고 싶다.",[18,1126,1127],{},"절차:",[44,1129,1130,1149,1152,1155],{},[47,1131,1132,1133,1136,1137,1140,1141,1144,1145,1148],{},"완전한 TTF 테이블 파싱: ",[35,1134,1135],{},"cmap"," (문자→글리프 매핑), ",[35,1138,1139],{},"glyf"," (아웃라인), ",[35,1142,1143],{},"loca"," (glyf 오프셋), ",[35,1146,1147],{},"hmtx"," (수평 메트릭) 등.",[47,1150,1151],{},"문서의 각 문자에 대해 cmap으로 글리프 ID 조회.",[47,1153,1154],{},"합성 글리프가 참조하는 하위 글리프를 추이적으로 수집.",[47,1156,1157],{},"그 글리프만 번호를 새로 매긴 TTF 출력.",[18,1159,1160,1161,1164,1165,1168],{},"핫패스는 2단계 — ",[22,1162,1163],{},"cmap 조회",". gofpdf 구현은 글리프 조회마다 cmap 테이블을 ",[22,1166,1167],{},"맨 위부터 걷는다",". Latin만 쓰는 페이지는 문제없다 — cmap이 작고 캐시가 잘 돈다. 150개 고유 글리프가 있는 CJK 페이지는 테이블 전체 주행을 150번 한다.",[18,1170,1171],{},"cmap format 12 (대부분의 현대 CJK 폰트가 사용)는 (start, end, startGlyphID) 트리플을 정렬한 배열. 1회 주행은 범위 수에 대해 O(n), NotoSansJP의 경우 200–500 범위. 150회 조회 × 400 범위당 비교 = 필요한 것보다 훨씬 많은 일.",[18,1173,1174,1175,1178],{},"gpdf는 폰트 최초 로드 시 cmap 전체를 ",[35,1176,1177],{},"map[rune]uint16","으로 풀어낸다. 이후 모든 조회는 O(1). NotoSansJP의 경우 일회성 비용 약 150 µs, 이후 문자당 10 ns.",[251,1180,1182],{"className":287,"code":1181,"language":289,"meta":259,"style":259},"// pdf/font/ttf.go 단순화\ntype Font struct {\n    runeToGID map[rune]uint16  // 로드 시 1회 해석\n    glyphs    []glyph          // GID로 인덱싱\n    metrics   []glyphMetric\n}\n\nfunc (f *Font) GlyphFor(r rune) uint16 {\n    return f.runeToGID[r]  // O(1), 캐시 친화적, 테이블 주행 없음\n}\n",[35,1183,1184,1189,1200,1219,1233,1243,1247,1251,1283,1306],{"__ignoreMap":259},[293,1185,1186],{"class":295,"line":296},[293,1187,1188],{"class":950},"// pdf/font/ttf.go 단순화\n",[293,1190,1191,1193,1196,1198],{"class":295,"line":351},[293,1192,888],{"class":299},[293,1194,1195],{"class":313}," Font",[293,1197,894],{"class":299},[293,1199,348],{"class":299},[293,1201,1202,1205,1208,1211,1213,1216],{"class":295,"line":377},[293,1203,1204],{"class":354},"    runeToGID ",[293,1206,1207],{"class":299},"map[",[293,1209,1210],{"class":330},"rune",[293,1212,931],{"class":299},[293,1214,1215],{"class":330},"uint16",[293,1217,1218],{"class":950},"  // 로드 시 1회 해석\n",[293,1220,1221,1224,1227,1230],{"class":295,"line":395},[293,1222,1223],{"class":354},"    glyphs    ",[293,1225,1226],{"class":299},"[]",[293,1228,1229],{"class":313},"glyph",[293,1231,1232],{"class":950},"          // GID로 인덱싱\n",[293,1234,1235,1238,1240],{"class":295,"line":421},[293,1236,1237],{"class":354},"    metrics   ",[293,1239,1226],{"class":299},[293,1241,1242],{"class":313},"glyphMetric\n",[293,1244,1245],{"class":295,"line":454},[293,1246,617],{"class":299},[293,1248,1249],{"class":295,"line":472},[293,1250,969],{"emptyLinePlaceholder":968},[293,1252,1253,1255,1257,1259,1261,1263,1265,1268,1270,1273,1276,1278,1281],{"class":295,"line":506},[293,1254,300],{"class":299},[293,1256,303],{"class":299},[293,1258,759],{"class":306},[293,1260,310],{"class":299},[293,1262,493],{"class":313},[293,1264,317],{"class":299},[293,1266,1267],{"class":320}," GlyphFor",[293,1269,324],{"class":299},[293,1271,1272],{"class":306},"r",[293,1274,1275],{"class":330}," rune",[293,1277,317],{"class":299},[293,1279,1280],{"class":330}," uint16",[293,1282,348],{"class":299},[293,1284,1285,1289,1292,1294,1297,1299,1301,1303],{"class":295,"line":556},[293,1286,1288],{"class":1287},"s7zQu","    return",[293,1290,1291],{"class":354}," f",[293,1293,277],{"class":299},[293,1295,1296],{"class":354},"runeToGID",[293,1298,924],{"class":299},[293,1300,1272],{"class":354},[293,1302,931],{"class":299},[293,1304,1305],{"class":950},"  // O(1), 캐시 친화적, 테이블 주행 없음\n",[293,1307,1308],{"class":295,"line":576},[293,1309,617],{"class":299},[18,1311,1312],{},"rune으로 인덱싱된 맵 하나, cmap 테이블의 선형 스캔 한 번으로 구성. 같은 폰트를 여러 페이지(보통 전 페이지)에 쓰는 문서에서 글리프 조회가 \"페이지 × 글리프의 거의 이차\"에서 \"총 글리프 수 + 상수\"로 바뀐다.",[681,1314,1316],{"id":1315},"왜-format-12가-중요한가","왜 \"format 12\"가 중요한가",[18,1318,1319,1320,1322,1323,1326,1327,1330],{},"많은 오래된 Go PDF 라이브러리는 Latin만 신경 쓰던 시절에 작성되었고, 구현한 cmap은 format 4 — Basic Multilingual Plane (U+0000–U+FFFF) 분할 범위. BMP 바깥의 일본어(드물지만 일부 이체 Kanji)는 format 12가 필요. ",[35,1321,124],{},"의 ",[35,1324,1325],{},"AddUTF8Font","는 NotoSansJP-Regular.ttf에서 ",[22,1328,1329],{},"panic","한다. format 12 파서가 끝까지 작성되지 않아서다.",[18,1332,1333],{},"이건 비방이 아니다. 유물이다. gofpdf는 2015년경 Latin 중심 웹 앱에 필요한 것으로서 훌륭한 라이브러리였고, 포크는 그 스코프를 그대로 물려받았다. 세상이 바뀌었다. CJK는 \"다른 사람 문제\"에서 \"일본어와 중국어 Go 생태계 다수의 문제\"가 되었다. gpdf는 cmap 명세를 완전히 구현했다. 하지 않으면 \"品目\"에 두부 박스가 박힌 인보이스가 생긴다 — 공개 첫 주에 실제 들어온 버그 리포트다.",[681,1335,1337],{"id":1336},"문서-수가-아닌-폰트-수로-스케일하는-캐시","문서 수가 아닌 폰트 수로 스케일하는 캐시",[18,1339,1340,1341,1344,1345,1347,1348,1351],{},"폰트 캐시는 ",[35,1342,1343],{},"Document","별이고 전역이 아니다. 같은 폰트로 PDF 10,000개 생성하면 150 µs 해석 비용을 10,000번 낸다 — 문서 간 ",[35,1346,493],{}," 인스턴스를 공유하지 않는 이상. API는 ",[35,1349,1350],{},"gpdf.WithSharedFont(preloadedFont)","를 지원.",[18,1353,1354,1355,1358],{},"고처리량 배치 생성(SaaS ",[35,1356,1357],{},"gpdf-api","가 이 방식)에서 공유 폰트 패턴이 P95 레이턴시를 예측 가능하게 만든다. 문서에 있다. OSS 사용자 대부분은 필요 없다.",[13,1360,1362],{"id":1361},"결합된-효과","결합된 효과",[18,1364,1365],{},"세 결정을 100페이지 벤치(gpdf 683 µs, gofpdf 11.7 ms)에 대입:",[104,1367,1368,1381],{},[107,1369,1370],{},[110,1371,1372,1375,1378],{},[113,1373,1374],{},"시간의 출처",[113,1376,1377],{},"gofpdf (페이지당 개략)",[113,1379,1380],{},"gpdf (페이지당 개략)",[132,1382,1383,1394,1405,1416,1427],{},[110,1384,1385,1388,1391],{},[137,1386,1387],{},"연산 슬라이스 구축",[137,1389,1390],{},"약 60 µs",[137,1392,1393],{},"0 (스트림 직접)",[110,1395,1396,1399,1402],{},[137,1397,1398],{},"연산 직렬화",[137,1400,1401],{},"약 35 µs",[137,1403,1404],{},"0 (이미 기록됨)",[110,1406,1407,1410,1413],{},[137,1408,1409],{},"글리프 조회 (40자)",[137,1411,1412],{},"약 6 µs",[137,1414,1415],{},"약 0.4 µs",[110,1417,1418,1421,1424],{},[137,1419,1420],{},"할당 / GC 압박",[137,1422,1423],{},"약 20 µs",[137,1425,1426],{},"약 2 µs",[110,1428,1429,1434,1439],{},[137,1430,1431],{},[22,1432,1433],{},"합계",[137,1435,1436],{},[22,1437,1438],{},"약 120 µs",[137,1440,1441],{},[22,1442,1443],{},"약 7 µs",[18,1445,1446,1447,1450],{},"숫자는 프로파일 추정이고 실제 분해는 내용에 따른다. 모양은 맞다. ",[22,1448,1449],{},"세 가지 중 어느 하나도 단독으로 10배를 내지 못한다",". 쌓여서 10배가 된다.",[18,1452,1453],{},"따라서: 기존 라이브러리에 하나만 복사해도 2–3배는 얻을 수 있다. 10배를 원하면 셋 다 필요하고, 첫 번째(단일 패스)를 AST 기반 라이브러리에 뒤늦게 넣는 건 재작성 말고는 길이 없다.",[13,1455,1457],{"id":1456},"포기한-것-정직한-섹션","포기한 것 (정직한 섹션)",[18,1459,1460],{},"빙빙 돌려 말했다. 전부 나열:",[18,1462,1463,1466,1467,1470],{},[22,1464,1465],{},"AST 기반 후처리."," 플러그인 아키텍처 없음. \"노드 트리 돌며 변환 적용\" 없음. 렌더링 전에 문서 전체 텍스트 스타일을 일괄 편집하고 싶으면 Builder 호출 ",[22,1468,1469],{},"전에"," 한다.",[18,1472,1473,1476,1477,1480],{},[22,1474,1475],{},"인트로스펙션."," ",[35,1478,1479],{},"doc.Components()","로 넣은 걸 돌려주는 메서드는 없다. 의미 있는 메서드가 돌아갈 수 있는 시점엔 문서가 이미 연산자 스트림이다. 대부분 사용자는 쓸 일 없다. 문서 조작 도구를 만드는 소수 사용자는 쓴다.",[18,1482,1483,1486,1487,1490,1491,1494],{},[22,1484,1485],{},"리플렉션 기반 직렬화."," 임의 struct를 PDF로 바꾸는 ",[35,1488,1489],{},"json.Unmarshal"," 스타일 API는 없다. JSON Schema 진입점(",[35,1492,1493],{},"template.FromJSON",")은 지원 형태를 명시한다. 의도적. 임의 Go struct를 넣어서 PDF를 받는 API가 필요하면 unidoc 영역.",[18,1496,1497,1476,1500,1502],{},[22,1498,1499],{},"interface의 확장성.",[35,1501,856],{},"를 구현해 커스텀 요소를 등록할 수 없다. Builder 호출을 감싸는 헬퍼 함수는 쓸 수 있다. 실용상 95% 요구를 커버하지만 모델이 다르다.",[18,1504,1505],{},"전부 의도한 결과다. 하나라도 채택하면 속도가 죽는다. \"빠르고 고집 있는 것\"이 맞는 사용자 버킷을 우선하고, \"유연하고 플러그인 풍부\"가 필요한 버킷은 Maroto v2나 unidoc이 더 맞는다.",[13,1507,1509],{"id":1508},"벤치-재현-가능한가","벤치 재현 가능한가",[18,1511,1512],{},"가능하다. 코드를 공개한 목적이 바로 그것이다.",[251,1514,1518],{"className":1515,"code":1516,"language":1517,"meta":259,"style":259},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https://github.com/gpdf-dev/gpdf\ncd gpdf/_benchmark\ngo test -bench=. -benchmem -benchtime=5s\n","bash",[35,1519,1520,1532,1540],{"__ignoreMap":259},[293,1521,1522,1525,1529],{"class":295,"line":296},[293,1523,1524],{"class":313},"git",[293,1526,1528],{"class":1527},"sfazB"," clone",[293,1530,1531],{"class":1527}," https://github.com/gpdf-dev/gpdf\n",[293,1533,1534,1537],{"class":295,"line":351},[293,1535,1536],{"class":320},"cd",[293,1538,1539],{"class":1527}," gpdf/_benchmark\n",[293,1541,1542,1544,1547,1550,1553],{"class":295,"line":377},[293,1543,289],{"class":313},[293,1545,1546],{"class":1527}," test",[293,1548,1549],{"class":1527}," -bench=.",[293,1551,1552],{"class":1527}," -benchmem",[293,1554,1555],{"class":1527}," -benchtime=5s\n",[18,1557,1558,1559,1564],{},"해당 디렉토리 README에 네 가지 워크로드와 측정 내용이 있다. 같은 CPU 아키텍처, 같은 Go 버전에서 20% 이상 차이가 나면 ",[76,1560,1563],{"href":1561,"rel":1562},"https://github.com/gpdf-dev/gpdf/issues",[80],"이슈","를 열어 주기 바란다 — drift는 실재한다.",[18,1566,1567],{},"두 가지 단서:",[1569,1570,1571,1577],"ul",{},[47,1572,1573,1574,1576],{},"벤치는 ",[35,1575,101],{},"와 함께 돈다. 끄면 전반적으로 약 5% 향상되지만, 실제 코드 운용 방식이 아니라 공개 수치에는 넣지 않는다.",[47,1578,1579],{},"CGO 비활성. CGO로 FreeType 백엔드를 붙이면 폰트 연산이 빨라질지 질문받아 실험했다. FFI 경계의 마샬링 비용이 이득을 삼켰다. PDF 생성기의 접근 패턴에는 순수 Go 서브세터가 이긴다.",[13,1581,1583],{"id":1582},"faq","FAQ",[18,1585,1586,1589,1590,1594],{},[22,1587,1588],{},"왜 아카이브된 gofpdf와 비교하나?","\n여전히 GitHub \"go pdf\" 검색 1위이고, gpdf로 착지하는 팀 대부분이 거기서 이주해 오기 때문. 벤치는 이 청중에 \"이주할 가치가 있나\"에 답해야 한다. 짧은 답: 있다. ",[76,1591,1593],{"href":1592},"/ko/blog/gofpdf-migration","이주 가이드","도 있다.",[18,1596,1597,1600],{},[22,1598,1599],{},"PDF 생성에서 10배 빠른 게 실질적으로 의미 있나?","\n워크로드에 따라. 요청당 한 문서면 — 딱히 없다, 양쪽 다 \"요청 경로에서 생성\" 임계를 넘는다. 배치(야간 명세, 대량 인보이스, DB 쿼리 기반 보고서 생성)에서는 격차가 그대로 머신 수 감소로 번역된다. 배치 파이프라인을 처음 이주한 팀에서 \"워커 수가 10분의 1\"이라는 피드백을 들었고, 계산을 감사하지 않았지만 벤치 모양과 정합한다.",[18,1602,1603,1606,1607,1610,1611,1613],{},[22,1604,1605],{},"CJK 숫자의 함정은?","\n폰트 파일은 직접 실어야 한다. gpdf가 서브세팅해 주지만 3 MB NotoSansJP TTF는 3 MB다. Go 바이너리에 임베드하거나 기동 시 ",[35,1608,1609],{},"os.ReadFile"," 한다. distroless 이미지에서는 영향을 준다. SaaS ",[35,1612,1357],{},"는 이미지에 대표 폰트를 동봉해 해결. OSS 사용자는 직접 다룬다.",[18,1615,1616,1619],{},[22,1617,1618],{},"기능이 늘면 느려지나?","\n가장 신경 쓰는 질문. 답: 릴리스마다 이전 버전과 벤치마크를 재고, 네 워크로드 중 하나라도 5% 이상 악화되면 릴리스를 막는다. 벤치가 라이브러리와 같은 리포지토리에 있는 이유가 바로 그것이다.",[18,1621,1622,1625],{},[22,1623,1624],{},"이름의 유래는?","\ngpdf = Go + PDF. 영리할 것 없다. 의도적으로 단순.",[13,1627,1629],{"id":1628},"gpdf를-써-본다","gpdf를 써 본다",[18,1631,1632],{},"gpdf는 PDF를 생성하는 Go 라이브러리다. MIT, 제로 의존성, 네이티브 CJK.",[251,1634,1636],{"className":1515,"code":1635,"language":1517,"meta":259,"style":259},"go get github.com/gpdf-dev/gpdf\n",[35,1637,1638],{"__ignoreMap":259},[293,1639,1640,1642,1645],{"class":295,"line":296},[293,1641,289],{"class":313},[293,1643,1644],{"class":1527}," get",[293,1646,1647],{"class":1527}," github.com/gpdf-dev/gpdf\n",[18,1649,1650,1655,1656],{},[76,1651,1654],{"href":1652,"rel":1653},"https://github.com/gpdf-dev/gpdf",[80],"⭐ Star on GitHub"," · ",[76,1657,1660],{"href":1658,"rel":1659},"https://gpdf.dev/ko/docs/quickstart",[80],"문서",[13,1662,1664],{"id":1663},"다음에-읽을-것","다음에 읽을 것",[1569,1666,1667,1674,1680],{},[47,1668,1669,1673],{},[76,1670,1672],{"href":1671},"/ko/blog/go-pdf-library-showdown-2026","2026 Go PDF 라이브러리 비교"," — 라이선스와 의존 포함 전체 라이브러리 비교.",[47,1675,1676,1679],{},[76,1677,1678],{"href":1592},"gofpdf는 아카이브됐다. gpdf로 이주하는 법"," — Before/After API 다섯 쌍, 전부 실행 가능.",[47,1681,1682,1683,277],{},"벤치마크 코드: ",[76,1684,1686],{"href":78,"rel":1685},[80],[35,1687,83],{},[1689,1690,1691],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":259,"searchDepth":351,"depth":351,"links":1693},[1694,1695,1696,1699,1703,1707,1708,1709,1710,1711,1712],{"id":15,"depth":351,"text":16},{"id":94,"depth":351,"text":95},{"id":245,"depth":351,"text":246,"children":1697},[1698],{"id":683,"depth":377,"text":684},{"id":734,"depth":351,"text":735,"children":1700},[1701,1702],{"id":1053,"depth":377,"text":1054},{"id":1092,"depth":377,"text":1093},{"id":1113,"depth":351,"text":1114,"children":1704},[1705,1706],{"id":1315,"depth":377,"text":1316},{"id":1336,"depth":377,"text":1337},{"id":1361,"depth":351,"text":1362},{"id":1456,"depth":351,"text":1457},{"id":1508,"depth":351,"text":1509},{"id":1582,"depth":351,"text":1583},{"id":1628,"depth":351,"text":1629},{"id":1663,"depth":351,"text":1664},"2026-04-19","단일 페이지 13 µs, 100페이지 보고서 683 µs. 튜닝이 아니라 세 가지 아키텍처 결정이 쌓인 결과. 실제 코드 경로를 짚어본다.",false,"md",null,{},"/ko/blog/why-gpdf-is-faster",{"title":5,"description":1714},"ko/blog/011.why-gpdf-is-faster",[1723,1724,1725],"benchmark","internals","comparison","vVcrmS2v5RV6IoJxeTe6TGcD31_YkrHCVVwFlaO6j2E",1776537637859]