Выполнение измерения ширины текста в AppKit

Есть ли способ в AppKit очень быстро измерить ширину большого количества объектов NSString (скажем, миллион)? Я пробовал 3 разных способа сделать это:

[NSString sizeWithAttributes:] [Размер NSAttributedString] NSLayoutManager (получите ширину текста вместо высоты)

Вот некоторые показатели производительности
Count\Mechanism    sizeWithAttributes    NSAttributedString    NSLayoutManager
1000               0.057                 0.031                 0.007
10000              0.329                 0.325                 0.064
100000             3.06                  3.14                  0.689
1000000            29.5                  31.3                  7.06



Очевидно, что NSLayoutManager - это путь, но проблема заключается в том, что

Высокий объем памяти (более 1 ГБ в соответствии с профилировщиком) из-за создания объектов NSTextStorage. Высокое время создания. Все время, затраченное на создание вышеприведенных строк, которое само по себе является самозахватом (в результате измерения объектов NSTextStorage, которые имеют глифы, созданные и выложенные, занимают всего около 0,0002 секунды). 7 секунд все еще слишком медленно для того, что я пытаюсь сделать. Есть ли более быстрый способ? Чтобы измерить миллион строк за секунду?

Если вы хотите поиграть, Здесь - проект github.

Ответ 1

Вот некоторые идеи, которые я не пробовал.

  • Используйте Основной текст напрямую. Другие API-интерфейсы построены поверх него.

  • Распараллеливание. Все современные Mac (и даже все современные устройства iOS) имеют несколько ядер. Разделите массив строк на несколько подмассивов. Для каждого subarray отправьте блок в глобальную очередь GCD . В блоке создайте необходимые объекты Core Text или NSLayoutManager и измерьте строки в подмассиве. Оба API могут использоваться безопасно таким образом. (основной текст) (NSLayoutManager)

  • Относительно "Высокий объем памяти": Использовать локальные блоки пула автообновления для уменьшения пикового объема памяти.

  • Относительно "Все время, затраченное на создание вышеуказанных строк, которое само по себе является самозахватом": вы говорите, что все время тратится на эти строки:

    double random = (double)arc4random_uniform(1000) / 1000;
    NSString *randomNumber = [NSString stringWithFormat:@"%f", random];
    

    Форматирование числа с плавающей запятой является дорогостоящим. Это ваш реальный прецедент? Если вы просто хотите отформатировать случайное рациональное выражение формы n/1000 для 0 ≤ n < 1000, есть более быстрые способы. Кроме того, во многих шрифтах все цифры имеют одинаковую ширину, поэтому легко набирать столбцы чисел. Если вы выберете такой шрифт, вы можете избежать измерения строк в первую очередь.

UPDATE

Здесь самый быстрый код, который я использовал с помощью Core Text. Отправленная версия почти в два раза быстрее, чем однопоточная версия на моем Core i7 MacBook Pro. Моя версия вашего проекта здесь.

static CGFloat maxWidthOfStringsUsingCTFramesetter(NSArray *strings, NSRange range) {
    NSString *bigString = [[strings subarrayWithRange:range] componentsJoinedByString:@"\n"];
    NSAttributedString *richText = [[NSAttributedString alloc] initWithString:bigString attributes:@{ NSFontAttributeName: (__bridge NSFont *)font }];
    CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), NULL);
    CGFloat width = 0.0;
    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)richText);
    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, bigString.length), path, NULL);
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
    for (id item in lines) {
        CTLineRef line = (__bridge CTLineRef)item;
        width = MAX(width, CTLineGetTypographicBounds(line, NULL, NULL, NULL));
    }
    CFRelease(frame);
    CFRelease(setter);
    CFRelease(path);
    return (CGFloat)width;
}

static void test_CTFramesetter() {
    runTest(__func__, ^{
        return maxWidthOfStringsUsingCTFramesetter(testStrings, NSMakeRange(0, testStrings.count));
    });
}

static void test_CTFramesetter_dispatched() {
    runTest(__func__, ^{
        dispatch_queue_t gatherQueue = dispatch_queue_create("test_CTFramesetter_dispatched result-gathering queue", nil);
        dispatch_queue_t runQueue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
        dispatch_group_t group = dispatch_group_create();

        __block CGFloat gatheredWidth = 0.0;

        const size_t Parallelism = 16;
        const size_t totalCount = testStrings.count;
        // Force unsigned long to get 64-bit math to avoid overflow for large totalCounts.
        for (unsigned long i = 0; i < Parallelism; ++i) {
            NSUInteger start = (totalCount * i) / Parallelism;
            NSUInteger end = (totalCount * (i + 1)) / Parallelism;
            NSRange range = NSMakeRange(start, end - start);
            dispatch_group_async(group, runQueue, ^{
                double width = maxWidthOfStringsUsingCTFramesetter(testStrings, range);
                dispatch_sync(gatherQueue, ^{
                    gatheredWidth = MAX(gatheredWidth, width);
                });
            });
        }

        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

        return gatheredWidth;
    });
}