Equivalent of Perl chomp?
Bengt Richter
bokr at oz.net
Mon Feb 4 03:15:11 EST 2002
On Sat, 2 Feb 2002 00:09:15 -0500, "Tim Peters" <tim.one at home.com> wrote:
>[Steve Holden]
>> 1,000,000 iterations.
>>
>> Testing for length 1:
>> Endswith 0.00000505
>> sliceIt 0.00000209
>>
>> Testing for length 2:
>> Endswith 0.00001137
>> sliceIt 0.00001148
>>
>> Length 3:
>> Endswith 0.00001055 [!]
>> sliceIt 0.00001264
>>
>> Of course, these tests are with the tests succeeding. I'm
>> presuming failure
>> would shorten the timings, but I'm not going to bother checking that
>> assumption.
>>
>> semi-believab-ly y'rs - steve
>
>This is the kind of fun I was hoping the OP would have. Good job! A lesson
>to take is that timing even seemingly trivial things can turn up major
>surprises with seemingly trivial changes in exactly what it is you're
>timing. The results are almost never accidental, but determining the true
>causes can leave you crawling on your belly for weeks begging for relief.
>
>A few things going on in this test under the covers:
>
>+ endswith does indeed pay a large price for building a bound
> method object every time. It's not only the dict lookup to
> resolve "endswith", but also malloc and free to build the
> bound method object and discard it afterwards.
>
>+ The huge increase in sliceit's time going from 1 to 2 is due to
> that s[:-1] creates a one-character string, and Python interns
ITYM s[-1:] ;-)
> those by magic: every instance of, e.g., 'a', no matter how
> computed, is exactly the same object (that's not actually true,
> but it's close <wink>). But 2-character computed strings are
> not interned, so s[:-2] has to malloc a fresh string object each
...similarly
> time to hold the two characters, and free it later. endswith
> doesn't have to build new string objects, so doesn't care as much.
>
>Now for some real fun: I have no idea why endswith took a huge jump going
I don't think _it_ really did (my results don't show it ;-).
>from 1 to 2. The saving grace for me is that it's so easy to find timing
>mysteries that I'd go mad if I felt compelled to chase them all down, so I
>can walk away without even looking back. I hope not everyone is so
>well-adjusted <wink>, though, as I'd like to know why.
>
I tried to eliminate spurious big times to get a better measure. My goal
was to get a minimum time, hoping that spurious time-consuming effects
in the scaffolding code (loops, hidden memory allocations/deallocations,
interrupt processing, etc.) would be near equivalent in the best case.
Then I tried to make the best case more likely for the timed segment.
It is interesting how hard it is to time things on the order of a few
microseconds properly in this kind of context.
Code to try to counter some of these effects is at the end of this post.
Suggestions for improvements welcome.
Whereas I like to count cycles for an algorithm, it _is_ easy to have the
interpreter fool you. Scaffolding code has cache effects, and it's easy
to generate a lot of cache-smashing activity with interpreted code.
Putting just the thing of interest inside a loop of a million causes
other effects, and you hope they get averaged out, but there can be some
large outliers in the average.
In C/C++ timing 'nothing' takes 32 cycles on my box. Using an extension to do the
same pattern in python takes about 450 cycles. Which of those extra cycles are
invalidating cache, and which aren't? It's pretty hard to say. OTOH, if there
were a VM bytecode which logged a timestamp in an internal pre-configured
circular buffer, it could be done in very few instructions, minimizing disturbance.
Anyway, there's more to getting net time than subtracting out the _apparent_ cost
of "timing nothing."
Say the minimum cycles for executing x is X, and that sampling time takes T
cycles, with Tb cycles before the instantaneous sample point (say when
rdtsc instruction completes), and Ta cycles after. Let's call a non-optimum
time T' to show some unknown cache miss effect etc. Then simple timing of x
gives us
dt = Ta' + X' + Tb'
I'm thinking that we can insert dummy T and X operations, on the assumption
that doing X' will assure as good a cache situation as possible for a
following X (therefore written without the [']) -- so we get
dt = T'*0 + Ta + X' + Tb'
(the '*0' meaning we ignore the value, just initializing cache so
we get predictable Ta at the beginning of the interval), and
dt2 = T'*0 + Ta + X' + X + Tb'
and then subtracting,
X = dt2-dt
We shouldn't have to know what the ['] effects are except to hope
that their _minimums_ would be closely repeatable and comparable in
the two sequences.
On this theory, I timed s.endswith and slice expression for
1- to 4-character tails. Also a "nothing" routine to subtract
the call/return time. I added gc just before each of the two
timing loops, and that seemed to narrow the variability a bit.
I also added an outer loop to average minimums found in the
inner loops and take advantage to get global minimums.
BTW, these are ticks at 300mhz on a P2 with 320MB RAM, with NT4.
[22:59] C:\pywk\crdtsc>python endsvslice.py 0 10 0 1000
10 loops, 1000 avgloops, 0 tadj
nothing: 850, avg 856
(I.e., ten loops in inner loop finding a minimum estimated X, and 1000
of those averaging the minimums, as well as finding the global minimums
whose difference might be a better estimate of X, where X is theoretical
best time for the function call.)
(The values below are adjusted by 850 for 'nothing', which is
a function returning a constant 1, so were estimating how much
longer it takes to prepare the 1 for return using endswith or
slice ==, compared to the constant 1. Non-result lines snipped.)
[23:00] C:\pywk\crdtsc>python endsvslice.py 1 10 850 1000
10 loops, 1000 avgloops, 850 tadj
endswith1: 1312, avg 1327
endswith2: 1335, avg 1366
endswith3: 1368, avg 1407
endswith4: 1432, avg 1388
slice1: 669, avg 740
slice2: 1898, avg 1942
slice3: 1937, avg 1934
slice4: 1915, avg 1907
IOW, endswith did not take a huge jump measuring my way, but
the slice was certainly affected from 1 to 2.
The little dip for slice4 is curious. Here are some more trials of that:
slice4: 1898, avg 1916
slice4: 1909, avg 1913
slice4: 1909, avg 1913
Hm, maybe slice3 above is off?
slice3: 1945, avg 1934
slice3: 1926, avg 1933
slice3: 1928, avg 1942
slice3: 1927, avg 1941
Curiouser and curiouser... anyway, no huge anomalies ;-)
Regards,
Bengt Richter
=======================================================================
Usage: python endsvslice.py id nloops adjust avgloops
where id is:
0: nothing
1: endswith1
2: endswith2
3: endswith3
4: endswith4
5: slice1
6: slice2
7: slice3
8: slice4
================ endsvslice.py ========================================
# endsvslice.py
import readtsc #experimental, provides access to pentium rdtsc
import gc
def nothing(s):
return 1
# brute force cut/paste to minimize scaffolding code
def endswith1(s):
return s.endswith('\n')
def endswith2(s):
return s.endswith(' \n')
def endswith3(s):
return s.endswith(' \n')
def endswith4(s):
return s.endswith(' \n')
def slice1(s):
return s[-1:] == '\n'
def slice2(s):
return s[-2:] == ' \n'
def slice3(s):
return s[-3:] == ' \n'
def slice4(s):
return s[-4:] == ' \n'
# Notation:
# X is minimum time, X' may be disturbed by cache miss etc.
# Tb is time before time sampling instant[1], Ta is time after
# T is total time in time sample code, hence T=Tb+Ta
# [1] e.g., instant might be defined as when rdtsc instruction copies counter
def t(loops,avgloops,func,arg):
# print loops,avgloops,func,len(arg)
r = readtsc.readtsc_dt # returns delta since last lower 32 bits of 64-bit time stamp
sum = sum2 = 0; gmin = gmin2 = 0x7fffffff
for aloop in xrange(avgloops):
dt = 0x7fffffff # not likely min
gc.collect() # attempt to normalize state somewhat
for i in xrange(loops):
r() # T' # Possibly non-optimal
r() # Ta # Tb is before, and Ta is hopefully run from cache
func(arg) # X' # x code not fully cached
e=r() # Tb' # Tb' time before sample may have cache miss etc.
dt = min(dt,e) # min (Ta + X' + Tb')
dt2 = 0x7fffffff # not likely min
gc.collect()
for i in xrange(loops): # same as above except x is run cached one time
r() # T'
r() # Ta
func(arg) # X'
func(arg) # X # x code hopefully cached after preceding X'
e=r() # Tb'
dt2 = min(dt2,e) # min (Ta + X' + X + Tb')
ddt = dt2-dt # X, more or less
sum += ddt
gmin = min(gmin,dt)
gmin2 = min(gmin2,dt2) # finding min X
return gmin2-gmin,(sum)/avgloops # should be X time for one clean func(arg) call
if __name__=='__main__':
import sys
selections = [nothing,endswith1,endswith2,endswith3,endswith4,
slice1,slice2,slice3,slice4]
try:
s='s'*1000000+'\n'; tadj = 0; loops = 10; avgloops = 1
id = int(sys.argv[1])
if len(sys.argv)>2: loops = int(sys.argv[2])
if len(sys.argv)>3: tadj= int(sys.argv[3])
if len(sys.argv)>4: avgloops= int(sys.argv[4])
tm,tav =t(loops,avgloops,selections[id],s)
print loops,'loops,',avgloops,'avgloops,',tadj,'tadj'
print '%10s: %5d, avg %5d' % (selections[id].__name__, tm-tadj, tav-tadj)
except:
print 'Usage: python endsvslice.py id nloops adjust avgloops'
print ' where id is:'
for i in xrange(len(selections)):
print ' %d: %s' %(i,selections[i].__name__)
=======================================================================
More information about the Python-list
mailing list