├── Sources ├── ZippyFormat.h └── ZIPStringFactory.m ├── Tests ├── Info.plist └── Tests.m ├── ZippyFormat.podspec ├── LICENSE ├── misc ├── benchmarks.txt └── chart.svg └── README.md /Sources/ZippyFormat.h: -------------------------------------------------------------------------------- 1 | @interface ZIPStringFactory : NSObject 2 | 3 | + (NSString *)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ZippyFormat.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ZippyFormat' 3 | s.version = '1.0.1' 4 | s.summary = 'A faster version of +[NSString stringWithFormat:]' 5 | 6 | s.description = <<-DESC 7 | ZippyFormat is a fast drop-in replacement for +[NSString stringWithFormat:] 8 | DESC 9 | 10 | s.homepage = 'https://github.com/michaeleisel/ZippyFormat' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { 'michaeleisel' => 'michael.eisel@gmail.com' } 13 | s.source = { :git => 'https://github.com/michaeleisel/ZippyFormat.git', :tag => s.version.to_s } 14 | 15 | s.ios.deployment_target = '11.0' 16 | s.osx.deployment_target = '10.13' 17 | 18 | s.source_files = 'Sources/**/*.{h,hh,mm,m,c,cpp}' 19 | s.requires_arc = false 20 | 21 | s.test_spec 'Tests' do |test_spec| 22 | # test_spec.requires_app_host = true 23 | test_spec.requires_arc = true 24 | test_spec.source_files = 'Tests/**/*.{swift,h,m}' 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Michael Eisel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /misc/benchmarks.txt: -------------------------------------------------------------------------------- 1 | The data was collected for the following tests (the order of the tests matches the order of the data) 2 | 3 | Tests, from Tests.m: 4 | PERF_TEST(@"%d", 2); 5 | PERF_TEST(@"%Lf", (long double)M_PI); 6 | PERF_TEST(@"%@", @"short"); 7 | PERF_TEST(@"%@", @"the quick brown fox jumped over the lazy dog"); 8 | PERF_TEST(@"%@%@", @"the quick brown fox ", @"jumped over the lazy dog"); 9 | PERF_TEST(@"%s%s", "the quick brown fox ", "jumped over the lazy dog"); 10 | PERF_TEST(@"the quick brown fox jumped over %d lazy dogs", 2500); 11 | PERF_TEST(@"%@", @{@"foo": @"bar"}); 12 | PERF_TEST(@"%@", @[@"foo", @"bar"]); 13 | PERF_TEST(@"%@", @2.5); 14 | NSString *hugeString = @"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 15 | PERF_TEST(@"%@%@%@%@", hugeString, hugeString, hugeString, hugeString); 16 | 17 | Data: 18 | Format Apple Time ZippyFormat Time 19 | one_int 2127085 7967108 20 | one_long_double 1774610 4278528 21 | short_string 2246189 9161073 22 | long_string 1729259 3957636 23 | two_strings 1562660 3142894 24 | two_c_strings 1541570 4367732 25 | int_long_string 1403588 3490133 26 | dictionary 615871 1981848 27 | array 608960 1980525 28 | nsnumber 706013 2592423 29 | huge_string 572223 1511433 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZippyFormat 2 | ## A faster version of +stringWithFormat: 3 | ![Coverage: 99%](https://img.shields.io/static/v1?label=coverage&message=99%&color=brightgreen) 4 | [![Cocoapods compatible](https://img.shields.io/badge/Cocoapods-compatible-4BC51D.svg?style=flat)](https://cocoapods.com) 5 | 6 | ## Benchmarks 7 | 8 | 9 | 10 | These benchmarks were done on an iPhone XS. The results on Mac are very similar. Note there are a few cases (positional arguments, `%S`, `%C`, malformed input) where ZippyFormat falls back to calling Apple's version. These cases, however, seem very uncommon for typical usage (e.g. localized strings with positional arguments would typically use`NSLocalizedString` instead). For more info, see [here](misc/benchmarks.txt). 11 | 12 | ## Usage 13 | 14 | Just add `#import ` at the top of the file and replace `[NSString stringWithFormat:...]` with `[ZIPStringFactory stringWithFormat:...]` wherever you want to use it. 15 | 16 | ## Why is it so much faster? 17 | 18 | - NSString grows an NSMutableString (or at least, its CoreFoundation relative) to create the string, whereas ZippyFormat appends directly into a `char *` buffer and only creates an NSString from it at the very end 19 | - ZippyFormat is able to use the stack for the `char *` up to a point, avoiding intermediate heap allocations that NSMutableString would make, since NSMutableString is always prepared to be used outside of its initial scope 20 | - Appends formatted arguments into the `char *` without performing validation because it already knows the data is valid UTF-8. NSMutableString's methods, on the other hand, are generic for other use cases and make fewer assumptions about the incoming bytes. This means additional unnecessary validation. 21 | - For `%@` arguments, NSString just appends `[object description]`. However, objects passed to debugging statements often consist of one of just a few classes (NSNumber, NSDictionary, NSArray, etc.). For these cases, ZippyFormat "inlines" the appending of the description by just copying the output it knows that it would consist of to the buffer, and doesn't call `[object description]` at all. 22 | 23 | So, it's largely due to Apple trying to be elegant and operate at a higher level. 24 | 25 | ## Installation 26 | 27 | ### Cocoapods 28 | 29 | ZippyFormat is available through [CocoaPods](https://cocoapods.org). To install 30 | it, simply add the following line to your Podfile: 31 | 32 | ```ruby 33 | pod 'ZippyFormat' 34 | ``` 35 | 36 | Note that since this is a new repo and Cocoapods can be slow at propagating changes, you may need to switch to their [CDN](https://blog.cocoapods.org/CocoaPods-1.7.2/) if you aren't using it already and if the project isn't being found. 37 | 38 | ### Manually 39 | 40 | ZippyFormat is just a few files, with no nested import structure or anything, so just copying the files in is pretty easy. 41 | 42 | ## Author 43 | 44 | Michael Eisel, michael.eisel@gmail.com 45 | -------------------------------------------------------------------------------- /Tests/Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.m 3 | // Tests 4 | // 5 | // Created by Michael Eisel on 11/28/20. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface CustomObject : NSObject 12 | @end 13 | 14 | @implementation CustomObject 15 | 16 | - (NSString *)description 17 | { 18 | return [ZIPStringFactory stringWithFormat:@"%@", @"foo"]; 19 | } 20 | 21 | @end 22 | 23 | @interface Tests : XCTestCase 24 | 25 | @end 26 | 27 | #define ARRAY_SIZE(x) sizeof(x) / sizeof(*(x)) 28 | 29 | #define PERF_TEST(title, format, ...) \ 30 | do { \ 31 | NSString *expected = [NSString stringWithFormat:format, __VA_ARGS__]; \ 32 | NSString *actual = [ZIPStringFactory stringWithFormat:format, __VA_ARGS__]; \ 33 | XCTAssert([expected isEqual:actual]); \ 34 | int limit = 5e4; \ 35 | NSLog(@"%@", format); \ 36 | for (int i = 0; i < 2; i++) { \ 37 | CFTimeInterval start1, start2, end1, end2; \ 38 | @autoreleasepool { \ 39 | start1 = CACurrentMediaTime(); \ 40 | for (int j = 0; j < limit; j++) { \ 41 | [NSString stringWithFormat:format, __VA_ARGS__]; \ 42 | } \ 43 | end1 = CACurrentMediaTime(); \ 44 | } \ 45 | @autoreleasepool { \ 46 | start2 = CACurrentMediaTime(); \ 47 | for (int j = 0; j < limit; j++) { \ 48 | [ZIPStringFactory stringWithFormat:format, __VA_ARGS__]; \ 49 | } \ 50 | end2 = CACurrentMediaTime(); \ 51 | } \ 52 | /*NSLog(@"mult: %@", @((end1 - start1) / (end2 - start2))); */\ 53 | /*NSLog(@"apple: %@, zippy: %@", @(1 / ((end1 - start1) / limit)), @(1 / ((end2 - start2) / limit)));*/ \ 54 | NSLog(@"%s\t%@\t%@", title, @(1 / ((end1 - start1) / limit)), @(1 / ((end2 - start2) / limit))); \ 55 | } \ 56 | } while(0) 57 | 58 | #define PERF_TEST2(code) \ 59 | do { \ 60 | int limit = 1e5; \ 61 | for (int i = 0; i < 1; i++) { \ 62 | NSLog(@"%s", #code); \ 63 | CFTimeInterval start1, end1; \ 64 | @autoreleasepool { \ 65 | start1 = CACurrentMediaTime(); \ 66 | for (int j = 0; j < limit; j++) { \ 67 | code; \ 68 | } \ 69 | end1 = CACurrentMediaTime(); \ 70 | } \ 71 | NSLog(@"%@", @(end1 - start1)); \ 72 | } \ 73 | } while(0) 74 | 75 | const int64_t kTestNums[] = {0, 1, -1, -2, 1ULL << 63, (1ULL << 63) - 1, 1ULL << 31, (1ULL << 31) - 1, 1ULL << 15, (1ULL << 15) - 1}; 76 | 77 | @implementation Tests 78 | 79 | static inline void testObject(id object) { 80 | NSString *expected = [NSString stringWithFormat:@"%@", object]; 81 | NSString *actual = [ZIPStringFactory stringWithFormat:@"%@", object]; 82 | XCTAssert([expected isEqual:actual]); 83 | } 84 | 85 | #define TEST(format, ...) \ 86 | do { \ 87 | NSString *expected = [NSString stringWithFormat:format, __VA_ARGS__]; \ 88 | NSString *actual = [ZIPStringFactory stringWithFormat:format, __VA_ARGS__]; \ 89 | XCTAssert([expected isEqual:actual]); \ 90 | } while(0) 91 | 92 | const NSStringEncoding encodings[] = {NSASCIIStringEncoding, NSNEXTSTEPStringEncoding, NSJapaneseEUCStringEncoding, NSUTF8StringEncoding, NSISOLatin1StringEncoding, NSSymbolStringEncoding, NSNonLossyASCIIStringEncoding, NSShiftJISStringEncoding, NSISOLatin2StringEncoding, NSUnicodeStringEncoding, NSWindowsCP1251StringEncoding, NSWindowsCP1252StringEncoding, NSWindowsCP1253StringEncoding, NSWindowsCP1254StringEncoding, NSWindowsCP1250StringEncoding, NSISO2022JPStringEncoding, NSMacOSRomanStringEncoding, NSUTF16StringEncoding, NSUTF16BigEndianStringEncoding, NSUTF16LittleEndianStringEncoding, NSUTF32StringEncoding, NSUTF32BigEndianStringEncoding, NSUTF32LittleEndianStringEncoding}; 93 | 94 | - (void)testAll 95 | { 96 | @autoreleasepool { 97 | [self runAllTests]; 98 | } 99 | } 100 | 101 | - (void)runAllTests 102 | { 103 | ({ 104 | ({ 105 | long long count = 0; 106 | TEST(@"%d%s%llnfoo%d", 2, "quick", &count, 2); 107 | }); 108 | long long count1 = 1; 109 | [NSString stringWithFormat:@"%s%llnfoo", "quick", &count1]; 110 | long long count2 = 1; 111 | [ZIPStringFactory stringWithFormat:@"%s%llnfoo%d", "quick", &count2]; 112 | XCTAssert(count1 == count2); 113 | }); 114 | 115 | // Bad 116 | ({ 117 | TEST(@"%d%v", 2); 118 | NSString *string = @"%d😊"; 119 | string = [string substringWithRange:NSMakeRange(0, [string length] - 1)]; 120 | TEST(@"%@", string); 121 | TEST(string, 2); 122 | unichar chars[5]; 123 | [@"foo" getBytes:chars maxLength:sizeof(chars) usedLength:NULL encoding:NSUTF16StringEncoding options:0 range:NSMakeRange(0, 3) remainingRange:NULL]; 124 | TEST(@"%C", chars[0]); 125 | TEST(@"%S", (const unichar *)chars); 126 | }); 127 | 128 | // Other 129 | ({ 130 | XCTAssert([@"" isEqual:[ZIPStringFactory stringWithFormat:@""]]); 131 | TEST(@"%2$d, %1$@", @"foo", 2); 132 | TEST(@"%@", @""); 133 | TEST(@"%d%d", 1, 2); 134 | TEST(@"%.02lf", (double)1.1); 135 | TEST(@"%.02lf", (double)M_PI); 136 | TEST(@"%1$lf", (double)M_PI); 137 | TEST(@"%c", 'a'); 138 | TEST(@"%c", 'a'); 139 | TEST(@"%s %s", "the quick brown", "fox"); 140 | TEST(@"%%%@", @""); 141 | TEST(@"%@", @""); 142 | TEST(@"%@", @""); 143 | TEST(@"the %@ brown %@ jumped over %@", @"quick", @"fox", @"the lazy dog"); 144 | TEST(@"the %@ brown %@ jumped over %@.", @"quick", @"fox", @"the lazy dog"); 145 | NSString *bigString = @"a"; 146 | for (NSInteger i = 0; i < 13; i++) { 147 | bigString = [bigString stringByAppendingString:bigString]; 148 | TEST(@"%@", bigString); 149 | NSString *nsFormat = [NSString stringWithFormat:@"%@%%1$d", bigString]; 150 | TEST(nsFormat, 3); 151 | //TEST(@"%@%@%@%@", bigString, bigString, bigString, bigString); 152 | NSString *expected = [NSString stringWithFormat:@"%@%@%@%@", bigString, bigString, bigString, bigString]; 153 | NSString *actual = [ZIPStringFactory stringWithFormat:@"%@%@%@%@", bigString, bigString, bigString, bigString]; 154 | XCTAssert([expected isEqual:actual]); 155 | } 156 | TEST(@"%@", @"😊"); 157 | const unichar chars[3] = {'a', 'b', 'c'}; 158 | TEST(@"%@", [NSString stringWithCharacters:chars length:3]); 159 | for (int i = 0; i < ARRAY_SIZE(encodings); i++) { 160 | NSStringEncoding encoding = encodings[i]; 161 | NSData *data = [@"the quick brown 😊 😊" dataUsingEncoding:encoding]; 162 | if (data) { 163 | TEST(@"%@", [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:encoding]); 164 | } 165 | } 166 | }); 167 | 168 | // Objects 169 | ({ 170 | testObject(nil); 171 | testObject([@"asdf" dataUsingEncoding:NSUTF8StringEncoding]); 172 | testObject([[CustomObject alloc] init]); 173 | testObject([NSNumber numberWithChar:-1]); 174 | testObject(@"asdf"); 175 | testObject(@1); 176 | testObject([NSNumber numberWithBool:YES]); 177 | testObject([NSNumber numberWithBool:NO]); 178 | testObject([NSNumber numberWithFloat:1.1]); 179 | testObject([NSNumber numberWithDouble:1.1]); 180 | testObject(@[@"asdf"]); 181 | testObject(@[@"asdf", @1, @[]]); 182 | testObject(@[]); 183 | testObject(@{}); 184 | testObject(@{@"a": @2}); 185 | testObject(@{@"a": @2, @"b": @3}); 186 | testObject(@{@"a": @2, @"b": @[@2, @{@"c": @[@"asdf"]}]}); 187 | for (int i = 0; i < ARRAY_SIZE(kTestNums); i++) { 188 | int64_t num = kTestNums[i]; 189 | testObject([NSNumber numberWithChar:num]); 190 | testObject([NSNumber numberWithUnsignedChar:num]); 191 | testObject([NSNumber numberWithInt:(int)num]); 192 | testObject([NSNumber numberWithUnsignedInt:(unsigned int)num]); 193 | testObject([NSNumber numberWithInteger:num]); 194 | testObject([NSNumber numberWithUnsignedLongLong:num]); 195 | testObject([NSNumber numberWithLongLong:num]); 196 | } 197 | }); 198 | 199 | // Floats 200 | ({ 201 | const char *sizeSpecs[] = {"L", "l", ""}; 202 | const char *numSpecs[] = {"a", "A", "e", "E", "f", "F", "g", "G"}; 203 | const long double nums[] = {0, 1, -1, -2, (long double)(1ULL << 63), (long double)((1ULL << 63) - 1), (long double)(1ULL << 31), (1ULL << 31) - 1, 1ULL << 15, (1ULL << 15) - 1, 0.1, 3.1415, -0.1, FLT_MAX, DBL_MAX, FLT_MIN, DBL_MIN, LDBL_MAX, LDBL_MIN, INFINITY, -INFINITY, NAN}; 204 | [ZIPStringFactory stringWithFormat:@"%a", (double)nums[7]]; 205 | for (int i = 0; i < ARRAY_SIZE(sizeSpecs); i++) { 206 | const char *sizeSpec = sizeSpecs[i]; 207 | for (int j = 0; j < ARRAY_SIZE(numSpecs); j++) { 208 | const char *numSpec = numSpecs[j]; 209 | for (int k = 0; k < ARRAY_SIZE(nums); k++) { 210 | if (strcmp(sizeSpec, "L") == 0) { 211 | long double num = nums[k]; 212 | NSString *nsFormat = [NSString stringWithFormat:@"%%%s%s", sizeSpec, numSpec]; 213 | NSString *actual = [ZIPStringFactory stringWithFormat:nsFormat, num]; 214 | NSString *expected = [NSString stringWithFormat:nsFormat, num]; 215 | // todo: investigate why Apple seems to differ incorrectly from printf for this specifier 216 | if (tolower(numSpec[0]) != 'a') { 217 | // LDBL_MAX, for example, seems unnecessarily truncated by Apple 218 | XCTAssert([actual isEqual:expected] || [expected length] > 500 && [actual hasPrefix:expected]); 219 | } 220 | } else if (strcmp(sizeSpec, "l") == 0) { 221 | double num = nums[k]; 222 | NSString *nsFormat = [NSString stringWithFormat:@"%%%s%s", sizeSpec, numSpec]; 223 | NSString *actual = [ZIPStringFactory stringWithFormat:nsFormat, num]; 224 | NSString *expected = [NSString stringWithFormat:nsFormat, num]; 225 | if (tolower(numSpec[0]) != 'a') { 226 | XCTAssert([actual isEqual:expected]); 227 | } 228 | } else { // float 229 | float num = nums[k]; 230 | NSString *nsFormat = [NSString stringWithFormat:@"%%%s%s", sizeSpec, numSpec]; 231 | NSString *actual = [ZIPStringFactory stringWithFormat:nsFormat, num]; 232 | NSString *expected = [NSString stringWithFormat:nsFormat, num]; 233 | if (tolower(numSpec[0]) != 'a') { 234 | XCTAssert([actual isEqual:expected]); 235 | } 236 | } 237 | } 238 | } 239 | } 240 | }); 241 | 242 | // Ints 243 | ({ 244 | const char *sizeSpecs[] = {"h", "hh", "l", "ll", "q", "z", "t", "j", ""}; 245 | const char *numSpecs[] = {"d", "u", "o", "x"}; 246 | for (int i = 0; i < ARRAY_SIZE(sizeSpecs); i++) { 247 | const char *sizeSpec = sizeSpecs[i]; 248 | for (int j = 0; j < ARRAY_SIZE(numSpecs); j++) { 249 | const char *numSpec = numSpecs[j]; 250 | for (int k = 0; k < ARRAY_SIZE(kTestNums); k++) { 251 | int64_t num = kTestNums[k]; 252 | NSString *nsFormat = [NSString stringWithFormat:@"%%%s%s", sizeSpec, numSpec]; 253 | NSString *actual = [ZIPStringFactory stringWithFormat:nsFormat, num]; 254 | NSString *expected = [NSString stringWithFormat:nsFormat, num]; 255 | XCTAssert([actual isEqual:expected]); 256 | } 257 | } 258 | } 259 | }); 260 | 261 | // Perf 262 | ({ 263 | PERF_TEST("one_int", @"%d", 2); 264 | PERF_TEST("one_long_double", @"%Lf", (long double)M_PI); 265 | PERF_TEST("short_string", @"%@", @"short"); 266 | PERF_TEST("long_string", @"%@", @"the quick brown fox jumped over the lazy dog"); 267 | PERF_TEST("two_strings", @"%@%@", @"the quick brown fox ", @"jumped over the lazy dog"); 268 | PERF_TEST("two_c_strings", @"%s%s", "the quick brown fox ", "jumped over the lazy dog"); 269 | PERF_TEST("int_long_string", @"the quick brown fox jumped over %d lazy dogs", 2500); 270 | PERF_TEST("dictionary", @"%@", @{@"foo": @"bar"}); 271 | PERF_TEST("array", @"%@", @[@"foo", @"bar"]); 272 | PERF_TEST("nsnumber", @"%@", @2.5); 273 | NSString *hugeString = @"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 274 | PERF_TEST("huge_string", @"%@%@%@%@", hugeString, hugeString, hugeString, hugeString); 275 | }); 276 | } 277 | 278 | @end 279 | -------------------------------------------------------------------------------- /Sources/ZIPStringFactory.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "ZippyFormat.h" 4 | #import 5 | 6 | #ifndef __LP64__ 7 | This is only meant for 64-bit systems 8 | #endif 9 | 10 | typedef struct { 11 | char *buffer; 12 | NSInteger length; 13 | NSInteger capacity; 14 | BOOL isStack; 15 | BOOL useApple; 16 | } String; 17 | 18 | static String stringCreate(char *stackString, NSInteger capacity) { 19 | String string = { 20 | .buffer = stackString, 21 | .length = 0, 22 | .capacity = capacity, 23 | .isStack = YES, 24 | .useApple = NO, 25 | }; 26 | return string; 27 | } 28 | 29 | static inline void stringEnsureExtraCapacity(String *string, NSInteger length) { 30 | NSInteger newLength = string->length + length; 31 | if (newLength <= string->capacity) { 32 | return; 33 | } 34 | NSInteger newCapacity = newLength * 2; 35 | char *newBuffer = malloc(newCapacity); 36 | memcpy(newBuffer, string->buffer, string->length); 37 | string->capacity = newCapacity; 38 | if (string->isStack) { 39 | string->isStack = NO; 40 | } else { 41 | free(string->buffer); 42 | } 43 | string->buffer = newBuffer; 44 | } 45 | 46 | #define APPEND_LITERAL(string, literal) \ 47 | do { \ 48 | _Static_assert(sizeof(literal) != 8, "literal looks like a pointer"); \ 49 | _Static_assert(sizeof(literal) != 0, "zero-length literal not allowed"); \ 50 | appendString(string, literal, sizeof(literal) - 1); \ 51 | } \ 52 | while (0) 53 | 54 | static void appendString(String *string, const char *src, NSInteger length) { 55 | stringEnsureExtraCapacity(string, length); 56 | memcpy(string->buffer + string->length, src, length); 57 | string->length += length; 58 | } 59 | 60 | static void appendChar(String *string, char c) { 61 | stringEnsureExtraCapacity(string, 1); 62 | string->buffer[string->length] = c; 63 | string->length++; 64 | } 65 | 66 | static bool writeNumberIfPossible(String *string, const char **formatPtr, va_list *args) { 67 | const char *format = *formatPtr; 68 | while (true) { 69 | char c = *format; 70 | if (c == '$' || c == '*' || c == '\0') { 71 | // Positional and * arguments not supported, and '\0' indicates malformed string 72 | return false; 73 | } 74 | char lower = tolower(c); 75 | if (lower == 'a' || lower == 'e' || lower == 'f' || lower == 'g' || lower == 'd' || lower == 'i' || lower == 'u' || lower == 'o' || lower == 'x' || lower == 'c' || lower == 'p' || lower == 'n') { 76 | if (lower == 'n') { 77 | va_arg(*args, void *); 78 | *formatPtr = format + 1; 79 | return true; 80 | } 81 | long length = format - (*formatPtr) + 3; 82 | char tempFormat[length]; 83 | memcpy(tempFormat, (*formatPtr) - 1, length - 1); 84 | tempFormat[length - 1] = '\0'; 85 | const int shortDestinationLength = 64; 86 | char shortDestination[shortDestinationLength]; 87 | int needed = 0; 88 | char prev = format[-1]; 89 | if (lower == 'd' || lower == 'i' || lower == 'u' || lower == 'o' || lower == 'x') { 90 | if (prev == 'l') { 91 | if (format[-2] == 'l') { 92 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, long long int)); 93 | } else { 94 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, long int)); 95 | } 96 | } else if (prev == 'q') { 97 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, long long)); 98 | } else if (prev == 'j') { 99 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, intmax_t)); 100 | } else if (prev == 'z') { 101 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, size_t)); 102 | } else if (prev == 't') { 103 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, ptrdiff_t)); 104 | } else { 105 | // Handles h and hh, and char and short get promoted to int anyways 106 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, int)); 107 | } 108 | } else if (lower == 'f' || lower == 'e' || lower == 'g' || lower == 'a') { 109 | if (prev == 'L') { 110 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, long double)); 111 | } else { 112 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, double)); 113 | } 114 | } else if (lower == 'c') { 115 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, int)); // char 116 | } else { 117 | needed = snprintf(shortDestination, shortDestinationLength, tempFormat, va_arg(*args, void *)); 118 | } 119 | if (needed < shortDestinationLength) { // If needed == destinationLength, the null terminator is the issue 120 | appendString(string, shortDestination, needed); 121 | *formatPtr = format + 1; 122 | } else { 123 | // Number was too large. This is pretty exceptional, i.e. a giant float 124 | return false; 125 | } 126 | return true; 127 | } 128 | format++; 129 | } 130 | return false; 131 | } 132 | 133 | static inline void appendNSString(String *string, NSString *nsString) { 134 | const char *cString = [nsString UTF8String]; 135 | if (cString) { 136 | appendString(string, cString, strlen(cString)); 137 | } else { 138 | string->useApple = true; 139 | } 140 | } 141 | 142 | static inline void appendNSNumber(String *string, NSNumber *number) { 143 | const char *typeStr = [number objCType]; 144 | char typeChar = typeStr[0]; 145 | if (typeChar != '\0' && typeStr[1] == 0) { 146 | if (typeChar == 'C' || typeChar == 'I' || typeChar == 'S' || typeChar == 'L' || typeChar == 'Q') { 147 | char buffer[20 /*digits*/ + 1 /*terminator*/]; 148 | snprintf(buffer, sizeof(buffer), "%llu", [number unsignedLongLongValue]); 149 | appendString(string, buffer, strlen(buffer)); 150 | return; 151 | } else if (typeChar == 'c' || typeChar == 'i' || typeChar == 's' || typeChar == 'l' || typeChar == 'q') { 152 | char buffer[19 /*digits*/ + 1 /*minus sign*/ + 1 /*terminator*/]; 153 | snprintf(buffer, sizeof(buffer), "%lld", [number longLongValue]); 154 | appendString(string, buffer, strlen(buffer)); 155 | return; 156 | } else if (typeChar == 'd' || typeChar == 'f') { 157 | double d = [number doubleValue]; 158 | const int bufferSize = 32; 159 | char buffer[bufferSize]; 160 | int needed = snprintf(buffer, bufferSize, "%g", d); 161 | if (needed < bufferSize) { 162 | appendString(string, buffer, strlen(buffer)); 163 | return; 164 | } 165 | } 166 | } 167 | appendNSString(string, [number description]); 168 | } 169 | 170 | static inline void appendNSArray(String *string, NSArray *array, int nestLevel); 171 | static inline void appendNSDictionary(String *string, NSDictionary *dictionary, int nestLevel); 172 | 173 | static Class nsObjectClass; 174 | static Class nsStringClass; 175 | static Class nsArrayClass; 176 | static Class nsDictionaryClass; 177 | static Class nsNumberClass; 178 | 179 | static void appendNSObject(String *string, id object, int nestLevel) { 180 | static dispatch_once_t onceToken; 181 | dispatch_once(&onceToken, ^{ 182 | nsObjectClass = [NSObject class]; 183 | nsStringClass = [NSString class]; 184 | nsArrayClass = [NSArray class]; 185 | nsDictionaryClass = [NSDictionary class]; 186 | nsNumberClass = [NSNumber class]; 187 | }); 188 | if (!object) { 189 | APPEND_LITERAL(string, "(null)"); 190 | } 191 | // To speed up class comparisons, just look at the classes in the object's class hierarchy whose depth we know to be the same as 192 | // as the class we're comparing against 193 | Class nearTopClass = [object class]; 194 | Class nearNearTopClass = nil; 195 | while (YES) { 196 | Class superclass = class_getSuperclass(nearTopClass); 197 | if (superclass == nsObjectClass || superclass == nil) { // superclass == nil if the root class here is, e.g., NSProxy 198 | break; 199 | } 200 | nearNearTopClass = nearTopClass; 201 | nearTopClass = superclass; 202 | } 203 | if (nearTopClass == nsArrayClass) { 204 | appendNSArray(string, object, nestLevel); 205 | } else if (nearTopClass == nsDictionaryClass) { 206 | appendNSDictionary(string, object, nestLevel); 207 | } else if (nearTopClass == nsStringClass) { 208 | appendNSString(string, object); 209 | } else if (nearNearTopClass == nsNumberClass) { // NSNumber -> NSValue -> NSObject 210 | appendNSNumber(string, object); 211 | } else { 212 | appendNSString(string, [object description]); 213 | } 214 | // More can always be added here, such as for NSData 215 | } 216 | 217 | static inline void appendNesting(String *string, int nestLevel) { 218 | for (int i = 0; i < nestLevel + 1; i++) { 219 | APPEND_LITERAL(string, " "); 220 | } 221 | } 222 | 223 | static inline void appendNSDictionary(String *string, NSDictionary *dictionary, int nestLevel) { 224 | appendNesting(string, nestLevel - 1); 225 | appendString(string, "{\n", 2); 226 | [dictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 227 | appendNesting(string, nestLevel); 228 | appendNSObject(string, key, nestLevel + 1); 229 | APPEND_LITERAL(string, " = "); 230 | appendNSObject(string, obj, nestLevel + 1); 231 | APPEND_LITERAL(string, ";\n"); 232 | }]; 233 | appendNesting(string, nestLevel - 1); 234 | appendChar(string, '}'); 235 | } 236 | 237 | static inline void appendNSArray(String *string, NSArray *array, int nestLevel) { 238 | appendNesting(string, nestLevel - 1); 239 | APPEND_LITERAL(string, "(\n"); 240 | NSInteger i = 0; 241 | NSInteger count = [array count]; 242 | // Use fast enumeration, with our own index, in case fast enumeration is better optimized, e.g. for retain/release 243 | for (id element in array) { 244 | appendNesting(string, nestLevel); 245 | appendNSObject(string, element, nestLevel + 1); 246 | if (i < count - 1) { 247 | appendChar(string, ','); 248 | } 249 | appendChar(string, '\n'); 250 | i++; 251 | } 252 | appendNesting(string, nestLevel - 1); 253 | appendChar(string, ')'); 254 | } 255 | 256 | static void appendObject(String *string, va_list *args) { 257 | id object = va_arg(*args, id); 258 | appendNSObject(string, object, 0); 259 | } 260 | 261 | NSString *ZIPStringWithFormatAndArguments(NSString *format, va_list *args) { 262 | va_list argsCopy; 263 | va_copy(argsCopy, *args); 264 | const NSInteger initialOutputCapacity = 500; 265 | char stackString[initialOutputCapacity]; 266 | String output = stringCreate(stackString, initialOutputCapacity); 267 | const char *cString = NULL; 268 | const char *formatCString = [format UTF8String]; 269 | if (formatCString) { 270 | cString = formatCString; 271 | } else { 272 | output.useApple = true; 273 | cString = ""; 274 | } 275 | NSInteger cStringLength = strlen(cString); 276 | const char *curr = cString; 277 | while (YES) { 278 | NSInteger remaining = cStringLength - (curr - cString); 279 | const char *next = memchr(curr, '%', remaining); 280 | if (!next) { 281 | appendString(&output, curr, remaining); 282 | break; 283 | } 284 | appendString(&output, curr, next - curr); 285 | curr = next + 1; 286 | switch (*curr) { 287 | case '@': 288 | appendObject(&output, args); 289 | curr++; 290 | break; 291 | case '%': 292 | appendChar(&output, '%'); 293 | curr++; 294 | break; 295 | case 'C': { 296 | output.useApple = YES; 297 | curr++; 298 | } break; 299 | case 's': { 300 | const char *str = va_arg(*args, char *); 301 | appendString(&output, str, strlen(str)); 302 | curr++; 303 | } break; 304 | case 'S': { 305 | output.useApple = YES; 306 | curr++; 307 | } break; 308 | default: { 309 | // This function assumes it's at the end, cannot have any legitimate cases after it 310 | bool appendDone = writeNumberIfPossible(&output, &curr, args); 311 | if (!appendDone) { 312 | // Format string appears malformed, which is UB, but just match Apple's treatment of the UB 313 | output.useApple = YES; 314 | } 315 | } break; 316 | } 317 | if (output.useApple) { 318 | break; 319 | } 320 | } 321 | if (output.useApple) { 322 | if (!output.isStack) { 323 | free(output.buffer); 324 | } 325 | NSString *string = [[[NSString alloc] initWithFormat:format arguments:argsCopy] autorelease]; 326 | va_end(argsCopy); 327 | return string; 328 | } 329 | va_end(argsCopy); 330 | if (output.isStack) { 331 | return CFBridgingRelease(CFStringCreateWithBytes(kCFAllocatorDefault, (UInt8 *)output.buffer, output.length, kCFStringEncodingUTF8, NO)); 332 | } else { 333 | return CFBridgingRelease(CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, (UInt8 *)output.buffer, output.length, kCFStringEncodingUTF8, NO, kCFAllocatorMalloc)); 334 | } 335 | } 336 | 337 | @implementation ZIPStringFactory 338 | 339 | + (NSString *)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); 340 | { 341 | va_list args; 342 | va_start(args, format); 343 | NSString *string = ZIPStringWithFormatAndArguments(format, &args); 344 | va_end(args); 345 | return string; 346 | } 347 | 348 | @end 349 | -------------------------------------------------------------------------------- /misc/chart.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------