{"id":13785100,"url":"https://github.com/MikePopoloski/StringFormatter","last_synced_at":"2025-05-11T20:32:02.070Z","repository":{"id":3946034,"uuid":"35918312","full_name":"MikePopoloski/StringFormatter","owner":"MikePopoloski","description":"Zero-allocation string formatting for .NET.","archived":true,"fork":false,"pushed_at":"2017-10-30T15:01:28.000Z","size":85,"stargazers_count":340,"open_issues_count":0,"forks_count":29,"subscribers_count":15,"default_branch":"master","last_synced_at":"2024-11-17T21:39:15.522Z","etag":null,"topics":["allocation","allocations-performed","performance","stringformatter"],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MikePopoloski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-05-20T01:26:15.000Z","updated_at":"2024-10-29T00:38:10.000Z","dependencies_parsed_at":"2022-08-28T10:00:52.907Z","dependency_job_id":null,"html_url":"https://github.com/MikePopoloski/StringFormatter","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MikePopoloski%2FStringFormatter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MikePopoloski%2FStringFormatter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MikePopoloski%2FStringFormatter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MikePopoloski%2FStringFormatter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MikePopoloski","download_url":"https://codeload.github.com/MikePopoloski/StringFormatter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253632067,"owners_count":21939370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["allocation","allocations-performed","performance","stringformatter"],"created_at":"2024-08-03T19:00:57.017Z","updated_at":"2025-05-11T20:32:01.735Z","avatar_url":"https://github.com/MikePopoloski.png","language":"C#","readme":"StringFormatter\n===============\n\nA zero-allocation* string formatting library for .NET applications.\n\nMotivation\n----------\n\nThe built-in string formatting facilities in .NET are robust and quite usable. Unfortunately, they also perform a ridiculous number of GC allocations.\nMostly these are short lived, and on the desktop GC they generally aren't noticeable. On more constrained systems however, they can be painful.\nAdditionally, if you're trying to track your GC usage via live reporting in your program, you might quickly notice that attempts to print out\nthe current GC state cause additional allocations, defeating the entire attempt at instrumentation.\n\nThus the existence of this library. It's not completely allocation free; there are several one-time setup costs. The steady state\nthough is entirely allocation-free. You can freely use the string formatting utilities in the main loop of a game without\nit causing a steady churn of garbage.\n\nQuick Start\n-----------\n\nThe library requires no installation, package management, or any other complicated distribution mechanism. Simply copy the [StringFormatter.cs](https://raw.githubusercontent.com/MikePopoloski/StringFormatter/master/StringFormatter.cs) file into your project and start using it.\n\nAt its simplest, you can make use of the static `StringBuffer.Format` convenience method. The `StringBuffer`\nformatting methods accept all of the formatting features supported by the .NET BCL.\n\n```csharp\nstring result = StringBuffer.Format(\"{0,-8:x} some text -- {1:C11} {2} more text here {3:G}\", -15, 13.4512m, true, double.MaxValue);\n// output:\n// \"-15      some text -- 13.4512 True more text here 1.79769313486232E+308\"\n```\n\n#### Allocation Analysis\n\nLet's look at the allocations performed by the previous example and compare them to the BCL's [string.Format](https://msdn.microsoft.com/en-us/library/zf3d0ccc(v=vs.110).aspx).\n\n|    | Mine | BCL | Explanation\n---|---|---|---\nParameters | 0 | 1+4 | Boxing value types plus `params[]` array allocation\nstatic `Format()` cache | 1 | 1 | Allocating a new `StringBuffer` / `StringBuilder` (will be cached in both cases)\nConstructor | 1 | 1 | Allocation of the backing `char[]` array.\nFormat specifiers | 0 | 3*3 | In the BCL, each specifier in the format string results in a new `StringBuilder` allocation, an underlying buffer allocation, and then a `ToString()` call.\nEach argument | 0 | 4 | The BCL calls `ToString()` on each argument.\n`ToString` | 1 | 1 | No way around it, if you want a `string` instance you need to allocate.\n\nTally them up, we get the following totals:\n\n|    | Mine | BCL\n---|---|---\nFirst Time | 3 | 21\nEach Additional | 1 | 19\n\nAt the steady state, `StringBuffer` requires 1 allocation per format call, regardless of the number of arguments. `StringBuilder` requires 2 + 5n, where n is the number of arguments.\nThere is an additional cost not mentioned in the above table: each type reallocates its internal buffer when the size of the resulting string grows too large.\nIf you set your capacity properly and `Clear()` your buffer between format operations (as the static `Format()` methods do) you can avoid this cost entirely.\n\nNote: that single allocation performed by `StringBuffer`, calling `ToString()` on the result, can be avoided by using additional library features described below.\n\nFeatures\n--------\n\n`StringBuffer` has a similar API to `StringBuilder`. You can create an instance and set a capacity and then reuse that buffer for many operations,\navoiding any allocations in the process. \n\n```csharp\nvar buffer = new StringBuffer(128);\nbuffer.Append(32.53);\nbuffer.Clear();\nbuffer.AppendFormat(\"{0}\", \"Foo\");\nvar result = buffer.ToString();\n```\n\n`StringBuffer` is fully culture-aware. Unlike the BCL APIs which require you to pass the desired `CultureInfo` around all over the\nplace, `StringBuffer` caches the culture during initialization and all subsequent formatting calls use it automatically.\nIf for some reason you want to mix and match strings for different cultures in the same buffer, you'll have to manage that yourself.\n\n(*) If you want to avoid even the one allocation incurred by calling `ToString()` on the result of the `StringBuffer`, you can make use\nof the `CopyTo` methods. These provide methods to copy the internal data to either managed buffers or to an arbitrary char pointer.\nYou can allocate stack memory or native heap memory and avoid any GC overhead entirely on a per-string basis:\n\n```csharp\nbuffer.Append(\"Hello\");\n\nvar output = stackalloc char[buffer.Count];\nbuffer.CopyTo(output, 0, buffer.Count);\n```\n\n#### Limitations\n\nUnlike in the BCL, each argument to `StringBuffer.AppendFormat` must either be one of the known built-in types or be a type implementing `IStringFormattable`.\nThis new interface is the analogue to the BCL's `IFormattable`. This restriction is part of how `StringBuffer` is able to avoid boxing arguments.\n\nIf you need to work with an existing type that you don't own, you can get around the restriction by using a *custom formatter*:\n\n```csharp \nStringBuffer.SetCustomFormatter\u003cMyType\u003e(FormatMyType);\n\nvoid FormatMyType(StringBuffer buffer, MyType value, StringView formatSpecifier) {\n}\n```\n\nOnce that call has been made, you may pass instances of `MyType` to any of the format methods.\n\nAnother limitation of `StringBuffer` is that there only exist `AppendFormat` methods taking up to 8 arguments. Adding additional ones is\ntrivial from a development perspective, but there does exist a statically compiled limit. Thus if you want to provide more, you\nneed to make use of the `AppendArgSet` method. This takes an instance of an `IArgSet`, which you must implement, and formats it according\nto the given format string. Whether or not this results in allocations is up to your implementation.\n\nThe format specifier for each argument is passed to the format routines via a `StringView`, which is a pointer to stack allocated\ntemporary memory. There is an upper limit to the size of this memory, so format specifiers are capped at a hard upper length.\nCurrently that length is 32, though it's easily changed in the source. Most format strings never have specifiers nearly that long; if\nyou're doing something crazy with specifiers though that might become a concern.\n\nPerformance\n-----------\n\nI need to do more in-depth performance analysis and comparisons, but so far my implementation is roughly on par with the BCL\nversions. Their formatting routines tend to be faster thanks to having hand-coded assembly routines in the CLR, but they\nalso allocate a lot more so it generally ends up being a wash.\n\nThere are a few cases where I know I'm significantly slower; for example, denormalized doubles aren't great. If your\napplication needs to format millions of denormalized numbers per second, you might want to consider sticking with the BCL.\n\nHere are some results obtained using BenchmarkDotNet for generating a fully formatted string:\n\nMachine info:\n```\nBenchmarkDotNet=v0.9.6.0\nOS=Microsoft Windows NT 6.2.9200.0\nProcessor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8\nFrequency=3312644 ticks, Resolution=301.8737 ns, Timer=TSC\nHostCLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]\nJitModules=clrjit-v4.6.1078.0\n```\n\nThe following test results compare `StringBuilder/StringFormat.AppendFormat` while returning a new allocated BCL string:\n\n**Type=StringFormatBenchmark  Mode=Throughput  Platform=X64**\n\n|       Method |       Jit |      Median |    StdDev | Scaled |         Min |         Max |  Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |\n|------------- |---------- |------------ |---------- |------- |------------ |------------ |------- |------ |------ |------------------- |\n|     Baseline | LegacyJit | 932.3745 ns | 6.5379 ns |   1.00 | 911.7104 ns | 941.6221 ns | 610.00 |     - |     - |             230.71 |\n|     Baseline |    RyuJit | 936.3304 ns | 6.2145 ns |   1.00 | 929.3742 ns | 950.8991 ns | 629.69 |     - |     - |             238.11 |\n| StringBuffer | LegacyJit | 824.8445 ns | 4.3413 ns |   0.88 | 817.6467 ns | 834.4629 ns | 133.87 |     - |     - |              52.75 |\n| StringBuffer |    RyuJit | 887.2168 ns | 8.8965 ns |   0.95 | 869.2266 ns | 910.2819 ns | 143.00 |     - |     - |              56.35 |\n\n\nThe following test results compare `StringBuilder/StringFormat.AppendFormat` without allocating, but rather reusing a target buffer for the string.\nThe main point of this test is to confirm that `StringFormatter` is indeed completely allocation-free when such a behavior is desired:\n\n**Type=NoAllocationBenchmark  Mode=Throughput  Platform=X64**\n\n|       Method |       Jit |      Median |     StdDev | Scaled |         Min |         Max |  Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |\n|------------- |---------- |------------ |----------- |------- |------------ |------------ |------- |------ |------ |------------------- |\n|     Baseline | LegacyJit | 913.6370 ns | 10.0141 ns |   1.00 | 898.5009 ns | 934.0547 ns | 410.37 |     - |     - |             157.63 |\n|     Baseline |    RyuJit | 920.6765 ns |  7.4793 ns |   1.00 | 903.9691 ns | 930.3559 ns | 401.64 |     - |     - |             154.28 |\n| NoAllocation | LegacyJit | 824.3390 ns |  5.8531 ns |   0.90 | 806.5347 ns | 833.2877 ns |      - |     - |     - |               0.11 |\n| NoAllocation |    RyuJit | 886.5322 ns |  3.7329 ns |   0.96 | 880.4307 ns | 895.9284 ns |      - |     - |     - |               0.11 |\n\nTo Do\n-----\n\nThere is still some work to be done:\n\n- General library cleanup and documentation\n- Flesh out the StringView type.\n- Unit tests\n- Improved error checking and exception messages\n- Custom numeric format strings\n- Enums\n- DateTime and TimeSpan\n- Switch to using UTF8 instead of UTF16? Might be nice.\n\nFeedback\n--------\n\nIf you have any comments, questions, or want to help out, feel free to get in touch or file an issue.\n","funding_links":[],"categories":["C\\#"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMikePopoloski%2FStringFormatter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMikePopoloski%2FStringFormatter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMikePopoloski%2FStringFormatter/lists"}