├── 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 | 
4 | [](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 |
--------------------------------------------------------------------------------