[{"data":1,"prerenderedAt":2627},["ShallowReactive",2],{"post-\u002Fblog\u002Frevisiting-boostexport":3},{"id":4,"title":5,"body":6,"book":2616,"date":2617,"description":2618,"extension":2619,"meta":2620,"navigation":233,"path":2621,"seo":2622,"stem":2623,"tags":2624,"__hash__":2626},"blog\u002Fblog\u002Frevisiting-boostexport.md","Revisiting boostexport",{"type":7,"value":8,"toc":2601},"minimark",[9,26,32,110,115,118,123,126,136,139,268,271,277,297,303,320,330,333,556,571,574,783,786,792,795,801,808,814,949,953,956,959,965,973,979,982,988,995,998,1163,1170,1173,1179,1183,1187,1190,1196,1199,1261,1276,1333,1336,1340,1343,1422,1425,1431,1434,1440,1443,1490,1493,1499,1502,1505,1620,1623,1800,1817,1820,1829,1832,1835,1883,1887,1890,1896,1899,1905,1908,2070,2073,2296,2303,2306,2310,2313,2316,2571,2574,2584,2590,2594,2597],[10,11,12,13,18,19,25],"p",{},"To understand what is written here, you need to read ",[14,15,17],"a",{"href":16},"\u002Fblog\u002Fbuilding-ml-inference-part-4","Building an ML Inference API, Part IV"," and be somewhat familiar with my project ",[14,20,24],{"href":21,"rel":22},"https:\u002F\u002Fgithub.com\u002Fhaiilong\u002Fboostexport",[23],"nofollow","boostexport",", which exports trained XGBoost, LightGBM, or CatBoost models into self-contained static classes for native inference in C# or Go.",[10,27,28],{},[29,30,31],"strong",{},"Sections:",[33,34,35,57,104],"ol",{},[36,37,38,42],"li",{},[14,39,41],{"href":40},"#i-how-to-represent-the-trees","How to represent the trees",[43,44,45,51],"ul",{},[36,46,47],{},[14,48,50],{"href":49},"#_1-data-array-form","Data-array form",[36,52,53],{},[14,54,56],{"href":55},"#_2-handling-oblivious-trees","Handling oblivious trees",[36,58,59,63],{},[14,60,62],{"href":61},"#ii-some-minor-performance-improvements","Some minor performance improvements",[43,64,65,71,77,92,98],{},[36,66,67],{},[14,68,70],{"href":69},"#_1-replace-sigmoid-for-binary-label","Replace sigmoid for binary label",[36,72,73],{},[14,74,76],{"href":75},"#_2-improve-fixed-depth-catboost-trees","Improve fixed-depth CatBoost trees",[36,78,79],{},[14,80,82,83,87,88,91],{"href":81},"#_3-use-byte-and-ushort-for-feature-indexes","Use ",[84,85,86],"code",{},"byte[]"," and ",[84,89,90],{},"ushort[]"," for Feature indexes",[36,93,94],{},[14,95,97],{"href":96},"#_4-use-local-variables-for-xgboost-multiclass-scores","Use local variables for XGBoost multiclass scores",[36,99,100],{},[14,101,103],{"href":102},"#_5-strange-behavior-for-multiclass-lightgbm","Strange behavior for multiclass LightGBM",[36,105,106],{},[14,107,109],{"href":108},"#iii-closing-note","Closing note",[111,112,114],"h2",{"id":113},"i-how-to-represent-the-trees","I. How to represent the trees",[10,116,117],{},"Reading Part IV again, I realized it never explained how the trees are laid out in the data-array form, so let's start there.",[119,120,122],"h3",{"id":121},"_1-data-array-form","1. Data-array form",[10,124,125],{},"Consider this small regression tree:",[127,128,134],"pre",{"className":129,"code":131,"language":132,"meta":133},[130],"language-text","                    f2 \u003C= 1.5?\n                   \u002F          \\\n             f0 \u003C= 0.2?       leaf: 1.2\n              \u002F       \\\n       leaf: -0.4    leaf: 0.8\n","text","",[84,135,131],{"__ignoreMap":133},[10,137,138],{},"Written directly in C#, it would look like this:",[127,140,144],{"className":141,"code":142,"language":143,"meta":133,"style":133},"language-csharp shiki shiki-themes one-light one-dark-pro","if (features[2] \u003C= 1.5)\n{\n    if (features[0] \u003C= 0.2)\n        return -0.4;\n\n    return 0.8;\n}\n\nreturn 1.2;\n","csharp",[84,145,146,183,189,213,228,235,246,252,257],{"__ignoreMap":133},[147,148,151,155,159,163,166,170,173,177,180],"span",{"class":149,"line":150},"line",1,[147,152,154],{"class":153},"sLKXg","if",[147,156,158],{"class":157},"s5ixo"," (",[147,160,162],{"class":161},"s7GmK","features",[147,164,165],{"class":157},"[",[147,167,169],{"class":168},"sAGMh","2",[147,171,172],{"class":157},"] ",[147,174,176],{"class":175},"sknuh","\u003C=",[147,178,179],{"class":168}," 1.5",[147,181,182],{"class":157},")\n",[147,184,186],{"class":149,"line":185},2,[147,187,188],{"class":157},"{\n",[147,190,192,195,197,199,201,204,206,208,211],{"class":149,"line":191},3,[147,193,194],{"class":153},"    if",[147,196,158],{"class":157},[147,198,162],{"class":161},[147,200,165],{"class":157},[147,202,203],{"class":168},"0",[147,205,172],{"class":157},[147,207,176],{"class":175},[147,209,210],{"class":168}," 0.2",[147,212,182],{"class":157},[147,214,216,219,222,225],{"class":149,"line":215},4,[147,217,218],{"class":153},"        return",[147,220,221],{"class":175}," -",[147,223,224],{"class":168},"0.4",[147,226,227],{"class":157},";\n",[147,229,231],{"class":149,"line":230},5,[147,232,234],{"emptyLinePlaceholder":233},true,"\n",[147,236,238,241,244],{"class":149,"line":237},6,[147,239,240],{"class":153},"    return",[147,242,243],{"class":168}," 0.8",[147,245,227],{"class":157},[147,247,249],{"class":149,"line":248},7,[147,250,251],{"class":157},"}\n",[147,253,255],{"class":149,"line":254},8,[147,256,234],{"emptyLinePlaceholder":233},[147,258,260,263,266],{"class":149,"line":259},9,[147,261,262],{"class":153},"return",[147,264,265],{"class":168}," 1.2",[147,267,227],{"class":157},[10,269,270],{},"First, give every node an integer index:",[127,272,275],{"className":273,"code":274,"language":132,"meta":133},[130],"Index 0: split on f2 \u003C= 1.5\nIndex 1: split on f0 \u003C= 0.2\nIndex 2: leaf -0.4\nIndex 3: leaf  0.8\nIndex 4: leaf  1.2\n",[84,276,274],{"__ignoreMap":133},[10,278,279,280,283,284,283,287,283,290,283,293,296],{},"The nodes are numbered in pre-order. Then they are stored in five arrays of length 5 (",[84,281,282],{},"Feature",", ",[84,285,286],{},"Threshold",[84,288,289],{},"Left",[84,291,292],{},"Right",[84,294,295],{},"Value","):",[127,298,301],{"className":299,"code":300,"language":132,"meta":133},[130],"Index        0     1     2     3     4\n----------------------------------------\nFeature      2     0     0     0     0\nThreshold   1.5   0.2   0.0   0.0   0.0\nLeft         1     2    -1    -1    -1\nRight        4     3     0     0     0\nValue       0.0   0.0  -0.4   0.8   1.2\n",[84,302,300],{"__ignoreMap":133},[10,304,305,306,308,309,311,312,87,314,316,317,319],{},"For a split node, ",[84,307,282],{}," is the feature index, ",[84,310,286],{}," is the value it compares against, and ",[84,313,289],{},[84,315,292],{}," are the indexes of its children. Its ",[84,318,295],{}," is always 0.",[10,321,322,323,326,327,329],{},"A leaf is marked with ",[84,324,325],{},"Left = -1",", and only its ",[84,328,295],{}," matters.",[10,331,332],{},"The generated C# is roughly:",[127,334,336],{"className":141,"code":335,"language":143,"meta":133,"style":133},"private static readonly byte[] Feature = [2, 0, 0, 0, 0];\nprivate static readonly double[] Threshold = [1.5, 0.2, 0.0, 0.0, 0.0];\nprivate static readonly int[] Left = [1, 2, -1, -1, -1];\nprivate static readonly int[] Right = [4, 3, 0, 0, 0];\nprivate static readonly double[] Value = [0.0, 0.0, -0.4, 0.8, 1.2];\n",[84,337,338,385,427,474,514],{"__ignoreMap":133},[147,339,340,343,346,349,352,355,358,361,364,366,368,370,372,374,376,378,380,382],{"class":149,"line":150},[147,341,342],{"class":153},"private",[147,344,345],{"class":153}," static",[147,347,348],{"class":153}," readonly",[147,350,351],{"class":153}," byte",[147,353,354],{"class":157},"[] ",[147,356,282],{"class":357},"sz0mV",[147,359,360],{"class":175}," =",[147,362,363],{"class":157}," [",[147,365,169],{"class":168},[147,367,283],{"class":157},[147,369,203],{"class":168},[147,371,283],{"class":157},[147,373,203],{"class":168},[147,375,283],{"class":157},[147,377,203],{"class":168},[147,379,283],{"class":157},[147,381,203],{"class":168},[147,383,384],{"class":157},"];\n",[147,386,387,389,391,393,396,398,400,402,404,407,409,412,414,417,419,421,423,425],{"class":149,"line":185},[147,388,342],{"class":153},[147,390,345],{"class":153},[147,392,348],{"class":153},[147,394,395],{"class":153}," double",[147,397,354],{"class":157},[147,399,286],{"class":357},[147,401,360],{"class":175},[147,403,363],{"class":157},[147,405,406],{"class":168},"1.5",[147,408,283],{"class":157},[147,410,411],{"class":168},"0.2",[147,413,283],{"class":157},[147,415,416],{"class":168},"0.0",[147,418,283],{"class":157},[147,420,416],{"class":168},[147,422,283],{"class":157},[147,424,416],{"class":168},[147,426,384],{"class":157},[147,428,429,431,433,435,438,440,442,444,446,449,451,453,455,458,460,462,464,466,468,470,472],{"class":149,"line":191},[147,430,342],{"class":153},[147,432,345],{"class":153},[147,434,348],{"class":153},[147,436,437],{"class":153}," int",[147,439,354],{"class":157},[147,441,289],{"class":357},[147,443,360],{"class":175},[147,445,363],{"class":157},[147,447,448],{"class":168},"1",[147,450,283],{"class":157},[147,452,169],{"class":168},[147,454,283],{"class":157},[147,456,457],{"class":175},"-",[147,459,448],{"class":168},[147,461,283],{"class":157},[147,463,457],{"class":175},[147,465,448],{"class":168},[147,467,283],{"class":157},[147,469,457],{"class":175},[147,471,448],{"class":168},[147,473,384],{"class":157},[147,475,476,478,480,482,484,486,488,490,492,495,497,500,502,504,506,508,510,512],{"class":149,"line":215},[147,477,342],{"class":153},[147,479,345],{"class":153},[147,481,348],{"class":153},[147,483,437],{"class":153},[147,485,354],{"class":157},[147,487,292],{"class":357},[147,489,360],{"class":175},[147,491,363],{"class":157},[147,493,494],{"class":168},"4",[147,496,283],{"class":157},[147,498,499],{"class":168},"3",[147,501,283],{"class":157},[147,503,203],{"class":168},[147,505,283],{"class":157},[147,507,203],{"class":168},[147,509,283],{"class":157},[147,511,203],{"class":168},[147,513,384],{"class":157},[147,515,516,518,520,522,524,526,528,530,532,534,536,538,540,542,544,546,549,551,554],{"class":149,"line":230},[147,517,342],{"class":153},[147,519,345],{"class":153},[147,521,348],{"class":153},[147,523,395],{"class":153},[147,525,354],{"class":157},[147,527,295],{"class":357},[147,529,360],{"class":175},[147,531,363],{"class":157},[147,533,416],{"class":168},[147,535,283],{"class":157},[147,537,416],{"class":168},[147,539,283],{"class":157},[147,541,457],{"class":175},[147,543,224],{"class":168},[147,545,283],{"class":157},[147,547,548],{"class":168},"0.8",[147,550,283],{"class":157},[147,552,553],{"class":168},"1.2",[147,555,384],{"class":157},[10,557,558,560,561,563,564,566,567,570],{},[84,559,282],{}," is a ",[84,562,86],{}," here because the model has fewer than 256 features. Larger models automatically use ",[84,565,90],{}," or ",[84,568,569],{},"int[]",". More on that in section II.",[10,572,573],{},"All asymmetric trees share one evaluator. It goes left or right by comparing against the threshold and returns when it reaches a leaf:",[127,575,577],{"className":141,"code":576,"language":143,"meta":133,"style":133},"private static double Eval(int node, ReadOnlySpan\u003Cdouble> features)\n{\n    while (true)\n    {\n        if (Left[node] \u003C 0)\n            return Value[node];\n\n        double value = features[Feature[node]];\n\n        if (value \u003C= Threshold[node])\n            node = Left[node];\n        else\n            node = Right[node];\n    }\n}\n",[84,578,579,619,623,635,640,663,677,681,705,709,733,750,756,772,778],{"__ignoreMap":133},[147,580,581,583,585,587,591,594,597,600,602,606,609,612,615,617],{"class":149,"line":150},[147,582,342],{"class":153},[147,584,345],{"class":153},[147,586,395],{"class":153},[147,588,590],{"class":589},"sAdtL"," Eval",[147,592,593],{"class":157},"(",[147,595,596],{"class":153},"int",[147,598,599],{"class":161}," node",[147,601,283],{"class":157},[147,603,605],{"class":604},"sC09Y","ReadOnlySpan",[147,607,608],{"class":157},"\u003C",[147,610,611],{"class":153},"double",[147,613,614],{"class":157},"> ",[147,616,162],{"class":161},[147,618,182],{"class":157},[147,620,621],{"class":149,"line":185},[147,622,188],{"class":157},[147,624,625,628,630,633],{"class":149,"line":191},[147,626,627],{"class":153},"    while",[147,629,158],{"class":157},[147,631,632],{"class":168},"true",[147,634,182],{"class":157},[147,636,637],{"class":149,"line":215},[147,638,639],{"class":157},"    {\n",[147,641,642,645,647,649,651,654,656,658,661],{"class":149,"line":230},[147,643,644],{"class":153},"        if",[147,646,158],{"class":157},[147,648,289],{"class":161},[147,650,165],{"class":157},[147,652,653],{"class":357},"node",[147,655,172],{"class":157},[147,657,608],{"class":175},[147,659,660],{"class":168}," 0",[147,662,182],{"class":157},[147,664,665,668,671,673,675],{"class":149,"line":237},[147,666,667],{"class":153},"            return",[147,669,670],{"class":161}," Value",[147,672,165],{"class":157},[147,674,653],{"class":357},[147,676,384],{"class":157},[147,678,679],{"class":149,"line":248},[147,680,234],{"emptyLinePlaceholder":233},[147,682,683,686,689,691,694,696,698,700,702],{"class":149,"line":254},[147,684,685],{"class":153},"        double",[147,687,688],{"class":357}," value",[147,690,360],{"class":175},[147,692,693],{"class":161}," features",[147,695,165],{"class":157},[147,697,282],{"class":161},[147,699,165],{"class":157},[147,701,653],{"class":357},[147,703,704],{"class":157},"]];\n",[147,706,707],{"class":149,"line":259},[147,708,234],{"emptyLinePlaceholder":233},[147,710,712,714,716,720,723,726,728,730],{"class":149,"line":711},10,[147,713,644],{"class":153},[147,715,158],{"class":157},[147,717,719],{"class":718},"sJa8x","value",[147,721,722],{"class":175}," \u003C=",[147,724,725],{"class":161}," Threshold",[147,727,165],{"class":157},[147,729,653],{"class":357},[147,731,732],{"class":157},"])\n",[147,734,736,739,741,744,746,748],{"class":149,"line":735},11,[147,737,738],{"class":357},"            node",[147,740,360],{"class":175},[147,742,743],{"class":161}," Left",[147,745,165],{"class":157},[147,747,653],{"class":357},[147,749,384],{"class":157},[147,751,753],{"class":149,"line":752},12,[147,754,755],{"class":153},"        else\n",[147,757,759,761,763,766,768,770],{"class":149,"line":758},13,[147,760,738],{"class":357},[147,762,360],{"class":175},[147,764,765],{"class":161}," Right",[147,767,165],{"class":157},[147,769,653],{"class":357},[147,771,384],{"class":157},[147,773,775],{"class":149,"line":774},14,[147,776,777],{"class":157},"    }\n",[147,779,781],{"class":149,"line":780},15,[147,782,251],{"class":157},[10,784,785],{},"Suppose the input is:",[127,787,790],{"className":788,"code":789,"language":132,"meta":133},[130],"f0 = 0.6\nf2 = 1.1\n",[84,791,789],{"__ignoreMap":133},[10,793,794],{},"The traversal is:",[127,796,799],{"className":797,"code":798,"language":132,"meta":133},[130],"Start at node 0\n\nnode 0:\n    f2 = 1.1\n    1.1 \u003C= 1.5 is true\n    move to Left[0], which is node 1\n\nnode 1:\n    f0 = 0.6\n    0.6 \u003C= 0.2 is false\n    move to Right[1], which is node 3\n\nnode 3:\n    Left[3] is -1, so this is a leaf\n    return Value[3], which is 0.8\n",[84,800,798],{"__ignoreMap":133},[10,802,803,804,807],{},"For a forest, every tree is appended to the same arrays, and a separate ",[84,805,806],{},"Roots"," array marks where each tree starts:",[127,809,812],{"className":810,"code":811,"language":132,"meta":133},[130],"All nodes:\n\n| tree 0 nodes | tree 1 nodes | tree 2 nodes | tree 3 nodes |\n  ^              ^              ^              ^\n  Roots[0]       Roots[1]       Roots[2]       Roots[3]\n",[84,813,811],{"__ignoreMap":133},[127,815,817],{"className":141,"code":816,"language":143,"meta":133,"style":133},"public static double Predict(ReadOnlySpan\u003Cdouble> features)\n{\n   double score = BaseScore;\n\n   for (int i = 0; i \u003C TreeCount; i++)\n       score += Eval(Roots[i], features);\n\n   return score;\n}\n",[84,818,819,845,849,864,868,905,932,936,945],{"__ignoreMap":133},[147,820,821,824,826,828,831,833,835,837,839,841,843],{"class":149,"line":150},[147,822,823],{"class":153},"public",[147,825,345],{"class":153},[147,827,395],{"class":153},[147,829,830],{"class":589}," Predict",[147,832,593],{"class":157},[147,834,605],{"class":604},[147,836,608],{"class":157},[147,838,611],{"class":153},[147,840,614],{"class":157},[147,842,162],{"class":161},[147,844,182],{"class":157},[147,846,847],{"class":149,"line":185},[147,848,188],{"class":157},[147,850,851,854,857,859,862],{"class":149,"line":191},[147,852,853],{"class":153},"   double",[147,855,856],{"class":357}," score",[147,858,360],{"class":175},[147,860,861],{"class":357}," BaseScore",[147,863,227],{"class":157},[147,865,866],{"class":149,"line":215},[147,867,234],{"emptyLinePlaceholder":233},[147,869,870,873,875,877,880,882,884,887,890,893,896,898,900,903],{"class":149,"line":230},[147,871,872],{"class":153},"   for",[147,874,158],{"class":157},[147,876,596],{"class":153},[147,878,879],{"class":357}," i",[147,881,360],{"class":175},[147,883,660],{"class":168},[147,885,886],{"class":157},"; ",[147,888,889],{"class":357},"i",[147,891,892],{"class":175}," \u003C",[147,894,895],{"class":357}," TreeCount",[147,897,886],{"class":157},[147,899,889],{"class":357},[147,901,902],{"class":175},"++",[147,904,182],{"class":157},[147,906,907,910,914,916,918,920,922,924,927,929],{"class":149,"line":237},[147,908,909],{"class":357},"       score",[147,911,913],{"class":912},"sblXP"," +=",[147,915,590],{"class":589},[147,917,593],{"class":157},[147,919,806],{"class":161},[147,921,165],{"class":157},[147,923,889],{"class":357},[147,925,926],{"class":157},"], ",[147,928,162],{"class":357},[147,930,931],{"class":157},");\n",[147,933,934],{"class":149,"line":248},[147,935,234],{"emptyLinePlaceholder":233},[147,937,938,941,943],{"class":149,"line":254},[147,939,940],{"class":153},"   return",[147,942,856],{"class":357},[147,944,227],{"class":157},[147,946,947],{"class":149,"line":259},[147,948,251],{"class":157},[119,950,952],{"id":951},"_2-handling-oblivious-trees","2. Handling oblivious trees",[10,954,955],{},"CatBoost normally trains symmetric trees (also called oblivious trees), where every node at a given level uses the same split.",[10,957,958],{},"A depth-three tree can be described by three questions:",[127,960,963],{"className":961,"code":962,"language":132,"meta":133},[130],"Level 0: f2 > 1.5?\nLevel 1: f0 > 0.2?\nLevel 2: f4 > 3.0?\n",[84,964,962],{"__ignoreMap":133},[10,966,967,968,87,970,972],{},"There is no need for ",[84,969,289],{},[84,971,292],{}," arrays. Each answer becomes one bit in the leaf index:",[127,974,977],{"className":975,"code":976,"language":132,"meta":133},[130],"bit 0 = result from level 0\nbit 1 = result from level 1\nbit 2 = result from level 2\n",[84,978,976],{"__ignoreMap":133},[10,980,981],{},"For example:",[127,983,986],{"className":984,"code":985,"language":132,"meta":133},[130],"Level 0: true  -> bit 0 = 1\nLevel 1: false -> bit 1 = 0\nLevel 2: true  -> bit 2 = 1\n\nLeaf bits:     101\nLeaf index:      5\n",[84,987,985],{"__ignoreMap":133},[10,989,990,991,994],{},"The prediction is ",[84,992,993],{},"LeafValues[5]",".",[10,996,997],{},"The evaluator builds that number with bitwise OR:",[127,999,1001],{"className":141,"code":1000,"language":143,"meta":133,"style":133},"int leafIndex = 0;\n\nif (features[feature0] > border0)\n    leafIndex |= 1; \u002F\u002F 001\n\nif (features[feature1] > border1)\n    leafIndex |= 2; \u002F\u002F 010\n\nif (features[feature2] > border2)\n    leafIndex |= 4; \u002F\u002F 100\n\nreturn LeafValues[leafOffset + leafIndex];\n",[84,1002,1003,1016,1020,1043,1060,1064,1086,1100,1104,1126,1140,1144],{"__ignoreMap":133},[147,1004,1005,1007,1010,1012,1014],{"class":149,"line":150},[147,1006,596],{"class":153},[147,1008,1009],{"class":357}," leafIndex",[147,1011,360],{"class":175},[147,1013,660],{"class":168},[147,1015,227],{"class":157},[147,1017,1018],{"class":149,"line":185},[147,1019,234],{"emptyLinePlaceholder":233},[147,1021,1022,1024,1026,1028,1030,1033,1035,1038,1041],{"class":149,"line":191},[147,1023,154],{"class":153},[147,1025,158],{"class":157},[147,1027,162],{"class":161},[147,1029,165],{"class":157},[147,1031,1032],{"class":357},"feature0",[147,1034,172],{"class":157},[147,1036,1037],{"class":175},">",[147,1039,1040],{"class":357}," border0",[147,1042,182],{"class":157},[147,1044,1045,1048,1051,1054,1056],{"class":149,"line":215},[147,1046,1047],{"class":357},"    leafIndex",[147,1049,1050],{"class":912}," |=",[147,1052,1053],{"class":168}," 1",[147,1055,886],{"class":157},[147,1057,1059],{"class":1058},"sW2Sy","\u002F\u002F 001\n",[147,1061,1062],{"class":149,"line":230},[147,1063,234],{"emptyLinePlaceholder":233},[147,1065,1066,1068,1070,1072,1074,1077,1079,1081,1084],{"class":149,"line":237},[147,1067,154],{"class":153},[147,1069,158],{"class":157},[147,1071,162],{"class":161},[147,1073,165],{"class":157},[147,1075,1076],{"class":357},"feature1",[147,1078,172],{"class":157},[147,1080,1037],{"class":175},[147,1082,1083],{"class":357}," border1",[147,1085,182],{"class":157},[147,1087,1088,1090,1092,1095,1097],{"class":149,"line":248},[147,1089,1047],{"class":357},[147,1091,1050],{"class":912},[147,1093,1094],{"class":168}," 2",[147,1096,886],{"class":157},[147,1098,1099],{"class":1058},"\u002F\u002F 010\n",[147,1101,1102],{"class":149,"line":254},[147,1103,234],{"emptyLinePlaceholder":233},[147,1105,1106,1108,1110,1112,1114,1117,1119,1121,1124],{"class":149,"line":259},[147,1107,154],{"class":153},[147,1109,158],{"class":157},[147,1111,162],{"class":161},[147,1113,165],{"class":157},[147,1115,1116],{"class":357},"feature2",[147,1118,172],{"class":157},[147,1120,1037],{"class":175},[147,1122,1123],{"class":357}," border2",[147,1125,182],{"class":157},[147,1127,1128,1130,1132,1135,1137],{"class":149,"line":711},[147,1129,1047],{"class":357},[147,1131,1050],{"class":912},[147,1133,1134],{"class":168}," 4",[147,1136,886],{"class":157},[147,1138,1139],{"class":1058},"\u002F\u002F 100\n",[147,1141,1142],{"class":149,"line":735},[147,1143,234],{"emptyLinePlaceholder":233},[147,1145,1146,1148,1151,1153,1156,1159,1161],{"class":149,"line":752},[147,1147,262],{"class":153},[147,1149,1150],{"class":161}," LeafValues",[147,1152,165],{"class":157},[147,1154,1155],{"class":357},"leafOffset",[147,1157,1158],{"class":175}," +",[147,1160,1009],{"class":357},[147,1162,384],{"class":157},[10,1164,1165,1166,1169],{},"A depth-three tree always has ",[84,1167,1168],{},"2³",", or eight, leaves. A depth-six tree has 64.",[10,1171,1172],{},"So the generated arrays look more like this:",[127,1174,1177],{"className":1175,"code":1176,"language":132,"meta":133},[130],"SplitFeature = [2, 0, 4, ...]\nSplitBorder  = [1.5, 0.2, 3.0, ...]\nLeafValues   = [leaf0, leaf1, ..., leaf7, ...]\n",[84,1178,1176],{"__ignoreMap":133},[111,1180,1182],{"id":1181},"ii-some-minor-performance-improvements","II. Some minor performance improvements",[119,1184,1186],{"id":1185},"_1-replace-sigmoid-for-binary-label","1. Replace sigmoid for binary label",[10,1188,1189],{},"A binary boosting model produces a raw score, often called a logit. Probability prediction applies sigmoid:",[127,1191,1194],{"className":1192,"code":1193,"language":132,"meta":133},[130],"sigmoid(x) = 1 \u002F (1 + exp(-x))\n",[84,1195,1193],{"__ignoreMap":133},[10,1197,1198],{},"The original label method called the probability method:",[127,1200,1202],{"className":141,"code":1201,"language":143,"meta":133,"style":133},"public static int Predict(ReadOnlySpan\u003Cdouble> features)\n    => PredictProbability(features) >= 0.5 ? 1 : 0;\n",[84,1203,1204,1228],{"__ignoreMap":133},[147,1205,1206,1208,1210,1212,1214,1216,1218,1220,1222,1224,1226],{"class":149,"line":150},[147,1207,823],{"class":153},[147,1209,345],{"class":153},[147,1211,437],{"class":153},[147,1213,830],{"class":589},[147,1215,593],{"class":157},[147,1217,605],{"class":604},[147,1219,608],{"class":157},[147,1221,611],{"class":153},[147,1223,614],{"class":157},[147,1225,162],{"class":161},[147,1227,182],{"class":157},[147,1229,1230,1233,1236,1238,1240,1243,1246,1249,1252,1254,1257,1259],{"class":149,"line":185},[147,1231,1232],{"class":157},"    => ",[147,1234,1235],{"class":589},"PredictProbability",[147,1237,593],{"class":157},[147,1239,162],{"class":357},[147,1241,1242],{"class":157},") ",[147,1244,1245],{"class":175},">=",[147,1247,1248],{"class":168}," 0.5",[147,1250,1251],{"class":157}," ? ",[147,1253,448],{"class":168},[147,1255,1256],{"class":157}," : ",[147,1258,203],{"class":168},[147,1260,227],{"class":157},[10,1262,1263,1264,1267,1268,1271,1272,1275],{},"That works, but it calls ",[84,1265,1266],{},"Math.Exp"," when the label doesn't need a probability. ",[84,1269,1270],{},"sigmoid(x) >= 0.5"," is the same as ",[84,1273,1274],{},"x >= 0"," (plot it on Desmos if you want to see it), so the sign of the raw score is enough:",[127,1277,1279],{"className":141,"code":1278,"language":143,"meta":133,"style":133},"public static int Predict(ReadOnlySpan\u003Cdouble> features)\n    => PredictScore(features) >= 0.0 ? 1 : 0;\n",[84,1280,1281,1305],{"__ignoreMap":133},[147,1282,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303],{"class":149,"line":150},[147,1284,823],{"class":153},[147,1286,345],{"class":153},[147,1288,437],{"class":153},[147,1290,830],{"class":589},[147,1292,593],{"class":157},[147,1294,605],{"class":604},[147,1296,608],{"class":157},[147,1298,611],{"class":153},[147,1300,614],{"class":157},[147,1302,162],{"class":161},[147,1304,182],{"class":157},[147,1306,1307,1309,1312,1314,1316,1318,1320,1323,1325,1327,1329,1331],{"class":149,"line":185},[147,1308,1232],{"class":157},[147,1310,1311],{"class":589},"PredictScore",[147,1313,593],{"class":157},[147,1315,162],{"class":357},[147,1317,1242],{"class":157},[147,1319,1245],{"class":175},[147,1321,1322],{"class":168}," 0.0",[147,1324,1251],{"class":157},[147,1326,448],{"class":168},[147,1328,1256],{"class":157},[147,1330,203],{"class":168},[147,1332,227],{"class":157},[10,1334,1335],{},"Probability prediction still computes sigmoid.",[119,1337,1339],{"id":1338},"_2-improve-fixed-depth-catboost-trees","2. Improve fixed-depth CatBoost trees",[10,1341,1342],{},"CatBoost's default trees are symmetric, so a depth-3 tree has 3 splits and 8 leaves. The original evaluator used arrays to find each tree's split range, depth, and leaf range:",[127,1344,1346],{"className":141,"code":1345,"language":143,"meta":133,"style":133},"int splitOffset = TreeSplitOffset[treeIndex];\nint depth = TreeDepth[treeIndex];\n\n\u002F\u002F Evaluate the tree...\n\nreturn LeafValues[TreeLeafOffset[treeIndex] + leafIndex];\n",[84,1347,1348,1367,1385,1389,1394,1398],{"__ignoreMap":133},[147,1349,1350,1352,1355,1357,1360,1362,1365],{"class":149,"line":150},[147,1351,596],{"class":153},[147,1353,1354],{"class":357}," splitOffset",[147,1356,360],{"class":175},[147,1358,1359],{"class":161}," TreeSplitOffset",[147,1361,165],{"class":157},[147,1363,1364],{"class":357},"treeIndex",[147,1366,384],{"class":157},[147,1368,1369,1371,1374,1376,1379,1381,1383],{"class":149,"line":185},[147,1370,596],{"class":153},[147,1372,1373],{"class":357}," depth",[147,1375,360],{"class":175},[147,1377,1378],{"class":161}," TreeDepth",[147,1380,165],{"class":157},[147,1382,1364],{"class":357},[147,1384,384],{"class":157},[147,1386,1387],{"class":149,"line":191},[147,1388,234],{"emptyLinePlaceholder":233},[147,1390,1391],{"class":149,"line":215},[147,1392,1393],{"class":1058},"\u002F\u002F Evaluate the tree...\n",[147,1395,1396],{"class":149,"line":230},[147,1397,234],{"emptyLinePlaceholder":233},[147,1399,1400,1402,1404,1406,1409,1411,1413,1415,1418,1420],{"class":149,"line":237},[147,1401,262],{"class":153},[147,1403,1150],{"class":161},[147,1405,165],{"class":157},[147,1407,1408],{"class":161},"TreeLeafOffset",[147,1410,165],{"class":157},[147,1412,1364],{"class":357},[147,1414,172],{"class":157},[147,1416,1417],{"class":175},"+",[147,1419,1009],{"class":357},[147,1421,384],{"class":157},[10,1423,1424],{},"This representation can handle trees with different depths, but many CatBoost models use one depth throughout:",[127,1426,1429],{"className":1427,"code":1428,"language":132,"meta":133},[130],"Tree 0: depth 3\nTree 1: depth 3\nTree 2: depth 3\n...\n",[84,1430,1428],{"__ignoreMap":133},[10,1432,1433],{},"Once the exporter knows the depth is uniform, the offsets are arithmetic:",[127,1435,1438],{"className":1436,"code":1437,"language":132,"meta":133},[130],"Tree 0 split offset = 0 × 3 = 0\nTree 1 split offset = 1 × 3 = 3\nTree 2 split offset = 2 × 3 = 6\n\nTree 0 leaf offset = 0 × 8 = 0\nTree 1 leaf offset = 1 × 8 = 8\nTree 2 leaf offset = 2 × 8 = 16\n",[84,1439,1437],{"__ignoreMap":133},[10,1441,1442],{},"The generated code becomes:",[127,1444,1446],{"className":141,"code":1445,"language":143,"meta":133,"style":133},"int splitOffset = treeIndex * TreeDepth;\nint leafOffset = treeIndex * (1 \u003C\u003C TreeDepth);\n",[84,1447,1448,1466],{"__ignoreMap":133},[147,1449,1450,1452,1454,1456,1459,1462,1464],{"class":149,"line":150},[147,1451,596],{"class":153},[147,1453,1354],{"class":357},[147,1455,360],{"class":175},[147,1457,1458],{"class":357}," treeIndex",[147,1460,1461],{"class":175}," *",[147,1463,1378],{"class":357},[147,1465,227],{"class":157},[147,1467,1468,1470,1473,1475,1477,1479,1481,1483,1486,1488],{"class":149,"line":185},[147,1469,596],{"class":153},[147,1471,1472],{"class":357}," leafOffset",[147,1474,360],{"class":175},[147,1476,1458],{"class":357},[147,1478,1461],{"class":175},[147,1480,158],{"class":157},[147,1482,448],{"class":168},[147,1484,1485],{"class":175}," \u003C\u003C",[147,1487,1378],{"class":357},[147,1489,931],{"class":157},[10,1491,1492],{},"This removes hot reads from three metadata arrays:",[127,1494,1497],{"className":1495,"code":1496,"language":132,"meta":133},[130],"TreeSplitOffset[]\nTreeDepth[]\nTreeLeafOffset[]\n",[84,1498,1496],{"__ignoreMap":133},[10,1500,1501],{},"The exporter checks the depths before choosing this form. A variable-depth model keeps the original metadata arrays and generic evaluator.",[10,1503,1504],{},"The level loop can go too. The generic code was:",[127,1506,1508],{"className":141,"code":1507,"language":143,"meta":133,"style":133},"for (int level = 0; level \u003C depth; level++)\n{\n    int split = splitOffset + level;\n\n    if (features[SplitFeature[split]] > SplitBorder[split])\n        leafIndex |= 1 \u003C\u003C level;\n}\n",[84,1509,1510,1543,1547,1565,1569,1601,1616],{"__ignoreMap":133},[147,1511,1512,1515,1517,1519,1522,1524,1526,1528,1531,1533,1535,1537,1539,1541],{"class":149,"line":150},[147,1513,1514],{"class":153},"for",[147,1516,158],{"class":157},[147,1518,596],{"class":153},[147,1520,1521],{"class":357}," level",[147,1523,360],{"class":175},[147,1525,660],{"class":168},[147,1527,886],{"class":157},[147,1529,1530],{"class":357},"level",[147,1532,892],{"class":175},[147,1534,1373],{"class":357},[147,1536,886],{"class":157},[147,1538,1530],{"class":357},[147,1540,902],{"class":175},[147,1542,182],{"class":157},[147,1544,1545],{"class":149,"line":185},[147,1546,188],{"class":157},[147,1548,1549,1552,1555,1557,1559,1561,1563],{"class":149,"line":191},[147,1550,1551],{"class":153},"    int",[147,1553,1554],{"class":357}," split",[147,1556,360],{"class":175},[147,1558,1354],{"class":357},[147,1560,1158],{"class":175},[147,1562,1521],{"class":357},[147,1564,227],{"class":157},[147,1566,1567],{"class":149,"line":215},[147,1568,234],{"emptyLinePlaceholder":233},[147,1570,1571,1573,1575,1577,1579,1582,1584,1587,1590,1592,1595,1597,1599],{"class":149,"line":230},[147,1572,194],{"class":153},[147,1574,158],{"class":157},[147,1576,162],{"class":161},[147,1578,165],{"class":157},[147,1580,1581],{"class":161},"SplitFeature",[147,1583,165],{"class":157},[147,1585,1586],{"class":357},"split",[147,1588,1589],{"class":157},"]] ",[147,1591,1037],{"class":175},[147,1593,1594],{"class":161}," SplitBorder",[147,1596,165],{"class":157},[147,1598,1586],{"class":357},[147,1600,732],{"class":157},[147,1602,1603,1606,1608,1610,1612,1614],{"class":149,"line":237},[147,1604,1605],{"class":357},"        leafIndex",[147,1607,1050],{"class":912},[147,1609,1053],{"class":168},[147,1611,1485],{"class":175},[147,1613,1521],{"class":357},[147,1615,227],{"class":157},[147,1617,1618],{"class":149,"line":248},[147,1619,251],{"class":157},[10,1621,1622],{},"For a known depth of three, the generator can write three fixed comparisons:",[127,1624,1626],{"className":141,"code":1625,"language":143,"meta":133,"style":133},"int split0 = splitOffset;\nif (features[SplitFeature[split0]] > SplitBorder[split0])\n    leafIndex |= 1;\n\nint split1 = splitOffset + 1;\nif (features[SplitFeature[split1]] > SplitBorder[split1])\n    leafIndex |= 2;\n\nint split2 = splitOffset + 2;\nif (features[SplitFeature[split2]] > SplitBorder[split2])\n    leafIndex |= 4;\n",[84,1627,1628,1641,1670,1680,1684,1701,1730,1740,1744,1761,1790],{"__ignoreMap":133},[147,1629,1630,1632,1635,1637,1639],{"class":149,"line":150},[147,1631,596],{"class":153},[147,1633,1634],{"class":357}," split0",[147,1636,360],{"class":175},[147,1638,1354],{"class":357},[147,1640,227],{"class":157},[147,1642,1643,1645,1647,1649,1651,1653,1655,1658,1660,1662,1664,1666,1668],{"class":149,"line":185},[147,1644,154],{"class":153},[147,1646,158],{"class":157},[147,1648,162],{"class":161},[147,1650,165],{"class":157},[147,1652,1581],{"class":161},[147,1654,165],{"class":157},[147,1656,1657],{"class":357},"split0",[147,1659,1589],{"class":157},[147,1661,1037],{"class":175},[147,1663,1594],{"class":161},[147,1665,165],{"class":157},[147,1667,1657],{"class":357},[147,1669,732],{"class":157},[147,1671,1672,1674,1676,1678],{"class":149,"line":191},[147,1673,1047],{"class":357},[147,1675,1050],{"class":912},[147,1677,1053],{"class":168},[147,1679,227],{"class":157},[147,1681,1682],{"class":149,"line":215},[147,1683,234],{"emptyLinePlaceholder":233},[147,1685,1686,1688,1691,1693,1695,1697,1699],{"class":149,"line":230},[147,1687,596],{"class":153},[147,1689,1690],{"class":357}," split1",[147,1692,360],{"class":175},[147,1694,1354],{"class":357},[147,1696,1158],{"class":175},[147,1698,1053],{"class":168},[147,1700,227],{"class":157},[147,1702,1703,1705,1707,1709,1711,1713,1715,1718,1720,1722,1724,1726,1728],{"class":149,"line":237},[147,1704,154],{"class":153},[147,1706,158],{"class":157},[147,1708,162],{"class":161},[147,1710,165],{"class":157},[147,1712,1581],{"class":161},[147,1714,165],{"class":157},[147,1716,1717],{"class":357},"split1",[147,1719,1589],{"class":157},[147,1721,1037],{"class":175},[147,1723,1594],{"class":161},[147,1725,165],{"class":157},[147,1727,1717],{"class":357},[147,1729,732],{"class":157},[147,1731,1732,1734,1736,1738],{"class":149,"line":248},[147,1733,1047],{"class":357},[147,1735,1050],{"class":912},[147,1737,1094],{"class":168},[147,1739,227],{"class":157},[147,1741,1742],{"class":149,"line":254},[147,1743,234],{"emptyLinePlaceholder":233},[147,1745,1746,1748,1751,1753,1755,1757,1759],{"class":149,"line":259},[147,1747,596],{"class":153},[147,1749,1750],{"class":357}," split2",[147,1752,360],{"class":175},[147,1754,1354],{"class":357},[147,1756,1158],{"class":175},[147,1758,1094],{"class":168},[147,1760,227],{"class":157},[147,1762,1763,1765,1767,1769,1771,1773,1775,1778,1780,1782,1784,1786,1788],{"class":149,"line":711},[147,1764,154],{"class":153},[147,1766,158],{"class":157},[147,1768,162],{"class":161},[147,1770,165],{"class":157},[147,1772,1581],{"class":161},[147,1774,165],{"class":157},[147,1776,1777],{"class":357},"split2",[147,1779,1589],{"class":157},[147,1781,1037],{"class":175},[147,1783,1594],{"class":161},[147,1785,165],{"class":157},[147,1787,1777],{"class":357},[147,1789,732],{"class":157},[147,1791,1792,1794,1796,1798],{"class":149,"line":735},[147,1793,1047],{"class":357},[147,1795,1050],{"class":912},[147,1797,1134],{"class":168},[147,1799,227],{"class":157},[10,1801,1802,1803,1806,1807,283,1810,87,1813,1816],{},"The fixed version drops ",[84,1804,1805],{},"level \u003C depth"," (compare + branch), ",[84,1808,1809],{},"level++",[84,1811,1812],{},"splitOffset + level",[84,1814,1815],{},"1 \u003C\u003C level"," (variable shift). The generated code grows with tree depth, not tree count. A model with 1,000 depth-3 trees still gets one 3-level helper rather than 1,000 separate blocks.",[10,1818,1819],{},"This should give a measurable gain, since RyuJIT's loop unroller won't do it for you (you can check with an IL Viewer).",[119,1821,1823,1824,1826,1827,91],{"id":1822},"_3-use-byte-and-ushort-for-feature-indexes","3. Use byte",[147,1825],{}," and ushort",[147,1828],{},[10,1830,1831],{},"The node-array evaluator reads a feature index at every split. Traversal jumps between nodes depending on feature values, so smaller indexes make it more likely the next one is already in cache.",[10,1833,1834],{},"The exporter now picks the array type based on the largest feature index:",[1836,1837,1838,1852],"table",{},[1839,1840,1841],"thead",{},[1842,1843,1844,1849],"tr",{},[1845,1846,1848],"th",{"align":1847},"right","Largest feature index",[1845,1850,1851],{},"Generated type",[1853,1854,1855,1865,1874],"tbody",{},[1842,1856,1857,1861],{},[1858,1859,1860],"td",{"align":1847},"0 to 255",[1858,1862,1863],{},[84,1864,86],{},[1842,1866,1867,1870],{},[1858,1868,1869],{"align":1847},"256 to 65,535",[1858,1871,1872],{},[84,1873,90],{},[1842,1875,1876,1879],{},[1858,1877,1878],{"align":1847},"Above 65,535",[1858,1880,1881],{},[84,1882,569],{},[119,1884,1886],{"id":1885},"_4-use-local-variables-for-xgboost-multiclass-scores","4. Use local variables for XGBoost multiclass scores",[10,1888,1889],{},"A multiclass model has one raw score per class. A three-class XGBoost model stores its trees interleaved by boosting round:",[127,1891,1894],{"className":1892,"code":1893,"language":132,"meta":133},[130],"Roots[0] = round 0, class 0\nRoots[1] = round 0, class 1\nRoots[2] = round 0, class 2\nRoots[3] = round 1, class 0\nRoots[4] = round 1, class 1\nRoots[5] = round 1, class 2\n...\n",[84,1895,1893],{"__ignoreMap":133},[10,1897,1898],{},"The root index is:",[127,1900,1903],{"className":1901,"code":1902,"language":132,"meta":133},[130],"round × number of classes + class\n",[84,1904,1902],{"__ignoreMap":133},[10,1906,1907],{},"The general evaluator accumulated into an array:",[127,1909,1911],{"className":141,"code":1910,"language":143,"meta":133,"style":133},"double[] scores = (double[])ClassBase.Clone();\n\nfor (int classIndex = 0; classIndex \u003C NumClasses; classIndex++)\n{\n    for (int tree = 0; tree \u003C TreesPerClass; tree++)\n    {\n        scores[classIndex] +=\n            Eval(Roots[classIndex + tree * NumClasses], features);\n    }\n}\n",[84,1912,1913,1942,1946,1979,1983,2017,2021,2035,2062,2066],{"__ignoreMap":133},[147,1914,1915,1917,1919,1922,1924,1926,1928,1931,1934,1936,1939],{"class":149,"line":150},[147,1916,611],{"class":153},[147,1918,354],{"class":157},[147,1920,1921],{"class":357},"scores",[147,1923,360],{"class":175},[147,1925,158],{"class":157},[147,1927,611],{"class":153},[147,1929,1930],{"class":157},"[])",[147,1932,1933],{"class":161},"ClassBase",[147,1935,994],{"class":157},[147,1937,1938],{"class":589},"Clone",[147,1940,1941],{"class":157},"();\n",[147,1943,1944],{"class":149,"line":185},[147,1945,234],{"emptyLinePlaceholder":233},[147,1947,1948,1950,1952,1954,1957,1959,1961,1963,1966,1968,1971,1973,1975,1977],{"class":149,"line":191},[147,1949,1514],{"class":153},[147,1951,158],{"class":157},[147,1953,596],{"class":153},[147,1955,1956],{"class":357}," classIndex",[147,1958,360],{"class":175},[147,1960,660],{"class":168},[147,1962,886],{"class":157},[147,1964,1965],{"class":357},"classIndex",[147,1967,892],{"class":175},[147,1969,1970],{"class":357}," NumClasses",[147,1972,886],{"class":157},[147,1974,1965],{"class":357},[147,1976,902],{"class":175},[147,1978,182],{"class":157},[147,1980,1981],{"class":149,"line":215},[147,1982,188],{"class":157},[147,1984,1985,1988,1990,1992,1995,1997,1999,2001,2004,2006,2009,2011,2013,2015],{"class":149,"line":230},[147,1986,1987],{"class":153},"    for",[147,1989,158],{"class":157},[147,1991,596],{"class":153},[147,1993,1994],{"class":357}," tree",[147,1996,360],{"class":175},[147,1998,660],{"class":168},[147,2000,886],{"class":157},[147,2002,2003],{"class":357},"tree",[147,2005,892],{"class":175},[147,2007,2008],{"class":357}," TreesPerClass",[147,2010,886],{"class":157},[147,2012,2003],{"class":357},[147,2014,902],{"class":175},[147,2016,182],{"class":157},[147,2018,2019],{"class":149,"line":237},[147,2020,639],{"class":157},[147,2022,2023,2026,2028,2030,2032],{"class":149,"line":248},[147,2024,2025],{"class":161},"        scores",[147,2027,165],{"class":157},[147,2029,1965],{"class":357},[147,2031,172],{"class":157},[147,2033,2034],{"class":912},"+=\n",[147,2036,2037,2040,2042,2044,2046,2048,2050,2052,2054,2056,2058,2060],{"class":149,"line":254},[147,2038,2039],{"class":589},"            Eval",[147,2041,593],{"class":157},[147,2043,806],{"class":161},[147,2045,165],{"class":157},[147,2047,1965],{"class":357},[147,2049,1158],{"class":175},[147,2051,1994],{"class":357},[147,2053,1461],{"class":175},[147,2055,1970],{"class":357},[147,2057,926],{"class":157},[147,2059,162],{"class":357},[147,2061,931],{"class":157},[147,2063,2064],{"class":149,"line":259},[147,2065,777],{"class":157},[147,2067,2068],{"class":149,"line":711},[147,2069,251],{"class":157},[10,2071,2072],{},"For a small, known class count, the generator now uses local variables:",[127,2074,2076],{"className":141,"code":2075,"language":143,"meta":133,"style":133},"double score0 = ClassBase[0];\ndouble score1 = ClassBase[1];\ndouble score2 = ClassBase[2];\n\nfor (int round = 0; round \u003C TreesPerClass; round++)\n{\n    int tree = round * NumClasses;\n\n    score0 += Eval(Roots[tree], features);\n    score1 += Eval(Roots[tree + 1], features);\n    score2 += Eval(Roots[tree + 2], features);\n}\n\nreturn [score0, score1, score2];\n",[84,2077,2078,2096,2113,2130,2134,2166,2170,2186,2190,2213,2240,2267,2271,2275],{"__ignoreMap":133},[147,2079,2080,2082,2085,2087,2090,2092,2094],{"class":149,"line":150},[147,2081,611],{"class":153},[147,2083,2084],{"class":357}," score0",[147,2086,360],{"class":175},[147,2088,2089],{"class":161}," ClassBase",[147,2091,165],{"class":157},[147,2093,203],{"class":168},[147,2095,384],{"class":157},[147,2097,2098,2100,2103,2105,2107,2109,2111],{"class":149,"line":185},[147,2099,611],{"class":153},[147,2101,2102],{"class":357}," score1",[147,2104,360],{"class":175},[147,2106,2089],{"class":161},[147,2108,165],{"class":157},[147,2110,448],{"class":168},[147,2112,384],{"class":157},[147,2114,2115,2117,2120,2122,2124,2126,2128],{"class":149,"line":191},[147,2116,611],{"class":153},[147,2118,2119],{"class":357}," score2",[147,2121,360],{"class":175},[147,2123,2089],{"class":161},[147,2125,165],{"class":157},[147,2127,169],{"class":168},[147,2129,384],{"class":157},[147,2131,2132],{"class":149,"line":215},[147,2133,234],{"emptyLinePlaceholder":233},[147,2135,2136,2138,2140,2142,2145,2147,2149,2151,2154,2156,2158,2160,2162,2164],{"class":149,"line":230},[147,2137,1514],{"class":153},[147,2139,158],{"class":157},[147,2141,596],{"class":153},[147,2143,2144],{"class":357}," round",[147,2146,360],{"class":175},[147,2148,660],{"class":168},[147,2150,886],{"class":157},[147,2152,2153],{"class":357},"round",[147,2155,892],{"class":175},[147,2157,2008],{"class":357},[147,2159,886],{"class":157},[147,2161,2153],{"class":357},[147,2163,902],{"class":175},[147,2165,182],{"class":157},[147,2167,2168],{"class":149,"line":237},[147,2169,188],{"class":157},[147,2171,2172,2174,2176,2178,2180,2182,2184],{"class":149,"line":248},[147,2173,1551],{"class":153},[147,2175,1994],{"class":357},[147,2177,360],{"class":175},[147,2179,2144],{"class":357},[147,2181,1461],{"class":175},[147,2183,1970],{"class":357},[147,2185,227],{"class":157},[147,2187,2188],{"class":149,"line":254},[147,2189,234],{"emptyLinePlaceholder":233},[147,2191,2192,2195,2197,2199,2201,2203,2205,2207,2209,2211],{"class":149,"line":259},[147,2193,2194],{"class":357},"    score0",[147,2196,913],{"class":912},[147,2198,590],{"class":589},[147,2200,593],{"class":157},[147,2202,806],{"class":161},[147,2204,165],{"class":157},[147,2206,2003],{"class":357},[147,2208,926],{"class":157},[147,2210,162],{"class":357},[147,2212,931],{"class":157},[147,2214,2215,2218,2220,2222,2224,2226,2228,2230,2232,2234,2236,2238],{"class":149,"line":711},[147,2216,2217],{"class":357},"    score1",[147,2219,913],{"class":912},[147,2221,590],{"class":589},[147,2223,593],{"class":157},[147,2225,806],{"class":161},[147,2227,165],{"class":157},[147,2229,2003],{"class":357},[147,2231,1158],{"class":175},[147,2233,1053],{"class":168},[147,2235,926],{"class":157},[147,2237,162],{"class":357},[147,2239,931],{"class":157},[147,2241,2242,2245,2247,2249,2251,2253,2255,2257,2259,2261,2263,2265],{"class":149,"line":735},[147,2243,2244],{"class":357},"    score2",[147,2246,913],{"class":912},[147,2248,590],{"class":589},[147,2250,593],{"class":157},[147,2252,806],{"class":161},[147,2254,165],{"class":157},[147,2256,2003],{"class":357},[147,2258,1158],{"class":175},[147,2260,1094],{"class":168},[147,2262,926],{"class":157},[147,2264,162],{"class":357},[147,2266,931],{"class":157},[147,2268,2269],{"class":149,"line":752},[147,2270,251],{"class":157},[147,2272,2273],{"class":149,"line":758},[147,2274,234],{"emptyLinePlaceholder":233},[147,2276,2277,2279,2281,2284,2286,2289,2291,2294],{"class":149,"line":774},[147,2278,262],{"class":153},[147,2280,363],{"class":157},[147,2282,2283],{"class":357},"score0",[147,2285,283],{"class":157},[147,2287,2288],{"class":357},"score1",[147,2290,283],{"class":157},[147,2292,2293],{"class":357},"score2",[147,2295,384],{"class":157},[10,2297,2298,2299,2302],{},"This removes the inner class loop and repeated ",[84,2300,2301],{},"scores[classIndex]"," access. The JIT can keep the three scores in registers, and tree traversal follows XGBoost's interleaved storage order.",[10,2304,2305],{},"This only kicks in for up to eight classes, so the generated methods don't get too big. Models with more classes keep the array loop.",[119,2307,2309],{"id":2308},"_5-strange-behavior-for-multiclass-lightgbm","5. Strange behavior for multiclass LightGBM",[10,2311,2312],{},"In benchmarks (using the models in the demo folder), this change improved XGBoost multiclass throughput by about 16% across repeated runs. The same change did nothing for LightGBM.",[10,2314,2315],{},"Splitting it into one loop per score did help, though:",[127,2317,2319],{"className":141,"code":2318,"language":143,"meta":133,"style":133},"double score0 = 0;\ndouble score1 = 0;\ndouble score2 = 0;\n\nfor (int tree = 0; tree \u003C TreesPerClass; tree++)\n    score0 += Eval(Roots[0 + tree * NumClasses], features);\n\nfor (int tree = 0; tree \u003C TreesPerClass; tree++)\n    score1 += Eval(Roots[1 + tree * NumClasses], features);\n\nfor (int tree = 0; tree \u003C TreesPerClass; tree++)\n    score2 += Eval(Roots[2 + tree * NumClasses], features);\n\nreturn [score0, score1, score2];\n",[84,2320,2321,2333,2345,2357,2361,2391,2421,2425,2455,2485,2489,2519,2549,2553],{"__ignoreMap":133},[147,2322,2323,2325,2327,2329,2331],{"class":149,"line":150},[147,2324,611],{"class":153},[147,2326,2084],{"class":357},[147,2328,360],{"class":175},[147,2330,660],{"class":168},[147,2332,227],{"class":157},[147,2334,2335,2337,2339,2341,2343],{"class":149,"line":185},[147,2336,611],{"class":153},[147,2338,2102],{"class":357},[147,2340,360],{"class":175},[147,2342,660],{"class":168},[147,2344,227],{"class":157},[147,2346,2347,2349,2351,2353,2355],{"class":149,"line":191},[147,2348,611],{"class":153},[147,2350,2119],{"class":357},[147,2352,360],{"class":175},[147,2354,660],{"class":168},[147,2356,227],{"class":157},[147,2358,2359],{"class":149,"line":215},[147,2360,234],{"emptyLinePlaceholder":233},[147,2362,2363,2365,2367,2369,2371,2373,2375,2377,2379,2381,2383,2385,2387,2389],{"class":149,"line":230},[147,2364,1514],{"class":153},[147,2366,158],{"class":157},[147,2368,596],{"class":153},[147,2370,1994],{"class":357},[147,2372,360],{"class":175},[147,2374,660],{"class":168},[147,2376,886],{"class":157},[147,2378,2003],{"class":357},[147,2380,892],{"class":175},[147,2382,2008],{"class":357},[147,2384,886],{"class":157},[147,2386,2003],{"class":357},[147,2388,902],{"class":175},[147,2390,182],{"class":157},[147,2392,2393,2395,2397,2399,2401,2403,2405,2407,2409,2411,2413,2415,2417,2419],{"class":149,"line":237},[147,2394,2194],{"class":357},[147,2396,913],{"class":912},[147,2398,590],{"class":589},[147,2400,593],{"class":157},[147,2402,806],{"class":161},[147,2404,165],{"class":157},[147,2406,203],{"class":168},[147,2408,1158],{"class":175},[147,2410,1994],{"class":357},[147,2412,1461],{"class":175},[147,2414,1970],{"class":357},[147,2416,926],{"class":157},[147,2418,162],{"class":357},[147,2420,931],{"class":157},[147,2422,2423],{"class":149,"line":248},[147,2424,234],{"emptyLinePlaceholder":233},[147,2426,2427,2429,2431,2433,2435,2437,2439,2441,2443,2445,2447,2449,2451,2453],{"class":149,"line":254},[147,2428,1514],{"class":153},[147,2430,158],{"class":157},[147,2432,596],{"class":153},[147,2434,1994],{"class":357},[147,2436,360],{"class":175},[147,2438,660],{"class":168},[147,2440,886],{"class":157},[147,2442,2003],{"class":357},[147,2444,892],{"class":175},[147,2446,2008],{"class":357},[147,2448,886],{"class":157},[147,2450,2003],{"class":357},[147,2452,902],{"class":175},[147,2454,182],{"class":157},[147,2456,2457,2459,2461,2463,2465,2467,2469,2471,2473,2475,2477,2479,2481,2483],{"class":149,"line":259},[147,2458,2217],{"class":357},[147,2460,913],{"class":912},[147,2462,590],{"class":589},[147,2464,593],{"class":157},[147,2466,806],{"class":161},[147,2468,165],{"class":157},[147,2470,448],{"class":168},[147,2472,1158],{"class":175},[147,2474,1994],{"class":357},[147,2476,1461],{"class":175},[147,2478,1970],{"class":357},[147,2480,926],{"class":157},[147,2482,162],{"class":357},[147,2484,931],{"class":157},[147,2486,2487],{"class":149,"line":711},[147,2488,234],{"emptyLinePlaceholder":233},[147,2490,2491,2493,2495,2497,2499,2501,2503,2505,2507,2509,2511,2513,2515,2517],{"class":149,"line":735},[147,2492,1514],{"class":153},[147,2494,158],{"class":157},[147,2496,596],{"class":153},[147,2498,1994],{"class":357},[147,2500,360],{"class":175},[147,2502,660],{"class":168},[147,2504,886],{"class":157},[147,2506,2003],{"class":357},[147,2508,892],{"class":175},[147,2510,2008],{"class":357},[147,2512,886],{"class":157},[147,2514,2003],{"class":357},[147,2516,902],{"class":175},[147,2518,182],{"class":157},[147,2520,2521,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2543,2545,2547],{"class":149,"line":752},[147,2522,2244],{"class":357},[147,2524,913],{"class":912},[147,2526,590],{"class":589},[147,2528,593],{"class":157},[147,2530,806],{"class":161},[147,2532,165],{"class":157},[147,2534,169],{"class":168},[147,2536,1158],{"class":175},[147,2538,1994],{"class":357},[147,2540,1461],{"class":175},[147,2542,1970],{"class":357},[147,2544,926],{"class":157},[147,2546,162],{"class":357},[147,2548,931],{"class":157},[147,2550,2551],{"class":149,"line":758},[147,2552,234],{"emptyLinePlaceholder":233},[147,2554,2555,2557,2559,2561,2563,2565,2567,2569],{"class":149,"line":774},[147,2556,262],{"class":153},[147,2558,363],{"class":157},[147,2560,2283],{"class":357},[147,2562,283],{"class":157},[147,2564,2288],{"class":357},[147,2566,283],{"class":157},[147,2568,2293],{"class":357},[147,2570,384],{"class":157},[10,2572,2573],{},"This version improved LightGBM multiclass by roughly 12%, even though it's basically the same code with the outer loop unrolled.",[10,2575,2576,2577,2580,2581,2583],{},"That's weird, because the interleaved version does strictly less loop work: one loop of N iterations with three bodies instead of three loops of N iterations. It makes the same number of ",[84,2578,2579],{},"Eval"," calls with one-third the loop increments and back-edges, keeps three independent FP accumulator chains instead of one, and reads ",[84,2582,806],{}," sequentially instead of strided. On paper it should win.",[10,2585,2586,2587,2589],{},"Since it loses, the cause has to be something order-sensitive inside ",[84,2588,2579],{}," (the split version evaluates all class-0 trees first, then all class-1 trees, and so on) or a codegen accident. Figuring out which would take me too far into JIT compiler design.",[111,2591,2593],{"id":2592},"iii-closing-note","III. Closing note",[10,2595,2596],{},"With these changes, I published boostexport v1.0.0. It should work fine for everything except uncommon objectives like poisson.",[2598,2599,2600],"style",{},"html pre.shiki code .sLKXg, html code.shiki .sLKXg{--shiki-default:#A626A4;--shiki-dark:#C678DD}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .s7GmK, html code.shiki .s7GmK{--shiki-default:#383A42;--shiki-dark:#E5C07B}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .sknuh, html code.shiki .sknuh{--shiki-default:#383A42;--shiki-dark:#56B6C2}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 .sz0mV, html code.shiki .sz0mV{--shiki-default:#383A42;--shiki-dark:#E06C75}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .sC09Y, html code.shiki .sC09Y{--shiki-default:#C18401;--shiki-dark:#E5C07B}html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}html pre.shiki code .sblXP, html code.shiki .sblXP{--shiki-default:#383A42;--shiki-dark:#C678DD}html pre.shiki code .sW2Sy, html code.shiki .sW2Sy{--shiki-default:#A0A1A7;--shiki-default-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic}",{"title":133,"searchDepth":185,"depth":185,"links":2602},[2603,2607,2615],{"id":113,"depth":185,"text":114,"children":2604},[2605,2606],{"id":121,"depth":191,"text":122},{"id":951,"depth":191,"text":952},{"id":1181,"depth":185,"text":1182,"children":2608},[2609,2610,2611,2613,2614],{"id":1185,"depth":191,"text":1186},{"id":1338,"depth":191,"text":1339},{"id":1822,"depth":191,"text":2612},"3. Use byte and ushort for Feature indexes",{"id":1885,"depth":191,"text":1886},{"id":2308,"depth":191,"text":2309},{"id":2592,"depth":185,"text":2593},null,"2026-08-03","Explaining the tree representation and some minor improvements","md",{},"\u002Fblog\u002Frevisiting-boostexport",{"title":5,"description":2618},"blog\u002Frevisiting-boostexport",[2625],"tech","tT1m6vOc3rJT2s_gf95vzalpRqeSrL2Ig4-3u1a7yDM",1792143571815]