As promised, I did some testing (sorry it took this long). This was run from my laptop in a Win2K3 Server VM. The results showed a pretty consistent run time of ~40 ms. Running the script in a loop of 20 times, the max ms were 50 and min was 30. The average was 40.45 and a median of 41. I will note that yes, there is a penalty for the first load of the tally table, which I saw the first time I ran it in tempdb. However, if it's used often, it will probably stay in memory, which it does in our system.
But to be fair, I did a DBCC freeproccache and DBCC dropcleanbuffers between the tests below and did experience the occasional doubling of time you experienced but it wasn't consistent.
Other changes made to your test script:
1) The udf takes nvarchar so I changed chars to nchar to remove the implicit conversion
2) Used datetime2
3) DATEDIFF for time difference calculation
4) Added GO 20 to repeat the script 20 times
declare @i int = 26, @x nvarchar(max) = N'', @d nchar(1) = N' ', @j-2 int;
declare @t table (id tinyint primary key, tokens int, which nvarchar(32), start datetime2, finish datetime2);
--Note: this is going to take a while, so if you want to run this more than once, store this data somewhere...
set @j-2 = @i*@i;
while @j-2 > 0 BEGIN
while @i > 0 BEGIN
set @x = @x + @d +NCHAR(91 - @i);
set @i = @i - 1;
END
set @i = 26
END
declare @C int;
update @t set tokens = @C, finish = sysdatetime() where id = 1;
insert into @t (id,which,start) values (2,'udf_StrList2Table',getdate());
select @C = COUNT(*) from ..[udf_StrList2Table] (@x,@d)
update @t set tokens = @C, finish = sysdatetime() where id = 2;
select *, DATEDIFF(millisecond, start, finish) as runtime from @t;
GO 20
/* Anything is possible but is it worth it? */