13 ms·
A single character code change
- gmfawcett 7y agoGood lord, this long-winded writing style is maddening to read! Here's what he changed: > Set<Thing> aSet = new HashSet<Thing>(); to: > Set<Thing> aSet = new HashSet<Thing>(0); His explanation: "Most of these sets were empty for the entirety of their life, but each one was consuming enough memory to hold the default number of entries. Huge numbers of these empty sets were created and together they consumed over half a gigabyte of memory. Over a third of our available heap." I'm not sure he ever got around to explaining how it saved "half a million dollars", though.
- awayfromhomenow 7y agono need to re-architect the program at that point, since heap pressure was reduced. The big takeaway for me here is to learn how your target language allocates memory for new objects.
- tlarkworthy 7y agoMy take home is focus on the thing that matters, and there is always low hanging fruit when something flips from "don't care" to "matters"
- nradov 7y agoIn general this is an example of how abstractions in high level languages become increasingly leaky as production workloads grow.
- davemp 7y ago> I'm not sure he ever got around to explaining how it saved "half a million dollars", though. Probably the classic software vanity formula: profit = efficiency
- sixothree 7y agoFTA: It involved some expensive re-architecturing. It required some very expensive new servers. It required a lot of even more expensive new licences for the third party components. It required delaying the features the business wanted until all this work was done.
- lioeters 7y agoI was about to write a similar comment but you wrote it a minute earlier. :) Seriously, the writing is long-winded and meandering. To add to the summary: "This particular trick also no longer works. As I found out while writing this article, modern JDKs do not allocate any space in a HashSet until something is added." So.. The article might have been better organized (and edited to 1/4 length) and focused on the section, "Four one liners every Java programmer should know". Unfortunately, it'd still be a click-baity title.
- sukilot 7y agoNumbers in headlines test better for SEO if they are odd, preferable prime.
- Tomte 7y ago> I'm not sure he ever got around to explaining how it saved "half a million dollars", though. Re-architecting the whole system, buying new licenses for third-party components, buying new servers (all explicitly named in the fine article) can easily cost half a million.
- deleted 7y ago[deleted]
- nradov 7y agoThe Java standard library also includes an empty set field, so as a performance optimization it's sometimes worth checking if your set will always be empty first and using that rather than instantiating a new HashSet at all. https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Collections.html#EMPTY_SET https://docs.oracle.com/en/java/javase/13/docs/api/java.base...
- merb 7y agowell the empty set is itself immutable, so depending on the code base the new HashSet(0) was less code.
- mannykannot 7y agoTo be fair, this is just an example to illustrate the larger point, which, to me, seems to be that it is sub-optimal to be dogmatic over statements like "premature optimization is the root of all evil". It seems plausible to me that halting the re-architecting project would produce the savings claimed.
- rhizome 7y agoNot all optimization is premature!
- mannykannot 7y agoPrecisely -- dogmatism fails because it does not deal with situations that call for sometimes subtle, case-by-case judgement.
- theonemind 7y agoIn general, I’ve found some of the worst thinking in otherwise smart people from ideological thinking. My self-destructing rule of thumb is “don’t be an ideologue”.
- gwd 7y agoBut this isn't at all an example of "premature optimization". There was a well-defined, long-term issue they were having, and someone fired up a profiler, and showed that a particular change had a massive impact. That's exactly the opposite of "premature optimization".
- mannykannot 7y agoI think it is supposed to be an example of just-in-time optimization that almost did not happen. The implication seems to be that the architects did not even consider code-level optimization as a possible solution. Maybe the better message is "try to understand the causes of your problem before you try to solve it."
- cryptonector 7y ago
- hinkley 7y agoThe section above describes all the hardware, new licenses, and labor needed to fix the problem by scaling up. If anything I’d bet he underestimated the labor and opportunity costs.
- michaelt 7y agoHe describes the re-architecturing which was partly motivated by memory usage issues, and the change he made which saved memory, but he doesn't explicitly say "this allowed us to avoid re-architecturing"
- deleted 7y ago[deleted]
- hinkley 7y agoThere are two types of people in the world. Those who can extrapolate from incomplete data... The cynic in me wants to blame crippling licensing fees. Have you ever scaled up Oracle? Just typing that question gave me a nervous twitch.
- rolltiide 7y ago> this long-winded writing style is maddening to read! I learned not to read/watch things that don’t get to the point Often times they never do!
- jfz 7y agoHow do you know that they won't get to the point if you don't read/watch them?
- rolltiide 7y agoThe comments, skimming/scrubbing, the presence of an obligatory tl;dr
- tedunangst 7y agoRead one in four articles all the way to the end.
- Merad 7y agoIf you read the article (what a novel idea!) the author explains that his company was running into heap size limits. The company was preparing to tackle this problem with a combination of extensive refactoring, server upgrades, licensing expensive third party libraries, and delaying new features while all of the above was done. His "optimization" reduced peak heap usage by half, making all the changes unnecessary.
- jcims 7y agoHave you ever noticed that it's becoming more difficult to find recipes without a six page blog entry on top of it? This is kind of like that. I love the word bloviating for times like this. It's not really an onomatopoeia but something like it. It's also somehow clearly an insult. Edit: jrockway has actually changed my perspective on recipe thing...if people want to put a six page backstory and photos on top of a recipe that's completely their right and I would never suggest that more quality (?) content is a bad thing. My problem, it seems, is with Google putting that as the top result when I'm searching for a recipe. Esp when I'm on my phone in a store getting spammed by some captive portal while trying to figure out if the damn cookies I'm making need baking soda or baking powder.
- ljm 7y agoI used to write a lot of blog posts and, when it was to do with work, there was always some professional writer or marketing guy who said it was better to begin the post with some barely relevant fluff. I didn’t agree, and it didn’t fit my own writing style. But they insisted I do it, or they’d edit it in themselves. Same with this post. The ‘context’ wasn’t particularly enlightening. I thought I’d be reading a programming war story but there wasn’t any pay off.
- combatentropy 7y agoI've studied writing all my life, and most of the classics on writing would agree with you, such as The Elements of Style and On Writing Well. William Zinsser, the author of On Writing Well and a newspaper editor, talks about this very thing, saying that articles are often improved by cutting the first three paragraphs.
- cpach 7y agoYeah what’s up with all these new long-winded recipe sites? Is it a SEO thing?
- redis_mlc 7y agoIt's a "two-part greeting" thing. But this is HN, so nobody knows what that is here. :)
- minimuffins 7y agoThis writing style is useful for beginners. Imagine a reader who's not already an expert, who isn't familiar with the decades worth of back and forth discourse about tradeoffs between performance and maintainability. He provides some context and history and tells a little story. Tech writing needs more of that. Half a million seems like a good guess for how much the company would have lost paying developers and architects to re-design and re-implement the system, buy new hardware, new licenses for their third party software and the revenue they would miss out on by doing all that unnecessary work instead of building a profitable new feature.
- jmilloy 7y ago> This writing style is useful for beginners. The most useful writing, for both beginner and expert audiences, puts the most important content first, with supporting details later. Dispersing the lede late in the article serves the author/advertisers, not the audience, and is independent of whether the content contains context for beginners or a narrative style.
- acvny 7y agoAll this in the name of the almighty SEO optimisation.
- rb808 7y agoNext step is to make it a 45 minute youtube video.
- AstroJetson 7y agoExactly!! "Hi Astro here, you are watching this video because you want to learn how to framis and being an expert in framis I'm going to help. But first click on the subscribe button and make sure that you watch my other videos. In them I explain ...... And while we are here about to learn how to framis, lets take a quick look at the new gizmo that I bought, it has nothing to do with framis, but it's cool ..... Wow that was cool. So lets get to the framis, did you remember to subscribe. To framis you do the following (11 seconds of explain). Good luck on your framis and don't forget to subscribe. Going to close out now with a video of the cat. /sigh
- cpach 7y agoThis is one of the reasons why I almost never watch stuff on YouTube. I heard people who claim that they learnt how to barbeque, renovate, take good photos etc. by watching YouTube. I’m impressed, but what I don’t understand is how they managed to either sit through or filter out this kind of chatter.
- sarah180 7y agoThere's a difference, because something like "take good photos" is not a task with an algorithm. If somebody wants to know how to resize an image in Photoshop, you really just want to know which option in which menu and which default settings to use, followed by short (skippable) discussions of sampling algorithms and how resolution interacts with image size. YouTube discourages this kind of efficient information delivery, as Google's incentives drive it to encourage sustained attention. It's hard to build that kind of attention on "press this button and then that one." This is analogous to a laboratory session where you really want to learn by doing, and just need the recipe. However, learning to be a good photographer is not an algorithmic task—though it may involve them. Being good at something complex involves a comprehensive view and a value system to understand what's important and how many factors interplay. This is calls for a soft version of the academic lecture—something YouTube can do very well. YouTube can be fantastic at the latter: just avoid it for the former.
- deleted 7y ago[deleted]
- blfr 7y agoMost books should be blog posts. Most blog posts should be tweets.
- cryptonector 7y ago> I'm not sure he ever got around to explaining how it saved "half a million dollars", though. Me either. TL;DR. I did skim enough to get to the one-character change, and I instantly knew what the issue was: they were using Java. Ok, ok, just kidding, yes, pre-allocating what you're not going to need is premature optimization -- don't do it, except when it pays off.
- thraxil 7y agoIt seems like you were reading the article with a mindset like "how do I save half a million dollars in my code with a one character change?" and were annoyed to find a bunch of other stuff that didn't directly answer your question. But if you actually read the whole thing, it's clearly a meditation on the classic tradeoff of readability vs performance optimization, when micro optimizations are and aren't worth it, and how we should be deciding when to focus on one aspect or another. The single character change thing was just one particularly salient example that he uses to drive a point home. Reading it without a preconceived idea and letting the article itself explain its purpose to me, I found it pretty well written. There's been plenty written about this tradeoff over the years and this was far from the worst I've seen.
- barbarbar 7y agoWish I had read this comment before the post. Thank you.
- raverbashing 7y agoPremature optimization is not great, but for low hanging fruit, just do it. As much as developer time is more expensive than machine time it seems people are forgetting best practices and/or minor improvements that can have a big impact.
- saalweachter 7y agoThis is somewhere where project style guides and code reviews can yield tiny, incremental wins.
- asveikau 7y agoThe article advises against applying this change everywhere, but I think this actually sounds like a good argument that the Java standard library should adjust the default constructor for Set. I am going to guess that Sets usually contain a small number of members. Paying memory allocation costs [amortized constant time] for large sets sounds a lot better than requiring memory to fit the possible range of values for small ones.
- nradov 7y agoWhat you're really asking for is to change the backing HashMap.DEFAULT_INITIAL_CAPACITY field. https://hg.openjdk.java.net/jdk/jdk14/file/f77e9e27b68d/src/java.base/share/classes/java/util/HashMap.java#l237 https://hg.openjdk.java.net/jdk/jdk14/file/f77e9e27b68d/src/... There's no hard data to show that a smaller value would give better performance for typical Java workloads. Resizing operations are quite expensive.
- stygiansonic 7y agoWhat's weird about the original article is that my understanding of HashMap (which as you pointed out, is the backing data structure of for HashSet) does not initialize the underlying hash table until first use - so if a bunch of new HashSets were created but never added to, the memory impact should be minimal irregardless of what the initial capacity was set to. (So there should not have been much of a difference between "new HashSet()" and "new HashSet(0)" if no items were ever added) See this comment, and also the implementation of putVal()/resize(): https://hg.openjdk.java.net/jdk/jdk14/file/f77e9e27b68d/src/java.base/share/classes/java/util/HashMap.java#l387 https://hg.openjdk.java.net/jdk/jdk14/file/f77e9e27b68d/src/... Perhaps things were different back in Java 1.3. EDIT: Apologies, I completely missed this in the article, where it's explicitly called out. This particular trick also no longer works. As I found out while writing this article, modern JDKs do not allocate any space in a HashSet until something is added. If I had kept applying this trick pre-emptively I would have been wasting my time.
- asveikau 7y agoIt sounds like you are squeezing my suggestion into a very specific solution. If there was a fix not tweaking current knobs but that made low numbers of elements cheap, while subsequently minimizing resizes when some reasonable heuristic for "mid-size to large" hash tables kicks in, you would oppose it universally? Resizing a small number of elements is not going to be bad.
- kinkrtyavimoodh 7y agoThere is a 'pre' in 'pre-mature' for a reason. If you don't seek to optimize even when it is 'mature', you are not a good software engineer. In this story, instead of doing meetings, why did no one fire up the profiler and actually try to figure out why the heap size was so big? If the senior architect's first thought is to come up with an elaborate proposal even before they understand the memory and processing budgets of their program, what are they a senior architect for? It's like trying to shave off micro seconds of processing time while your network call takes milliseconds. Good style guides strike the balance where common caveats and easy to miss gotchas are avoided by making rules around them (and then it's no longer premature optimization because someone already did the math and decided on the optimization, so each new programmer using it is not optimizing but merely following a rule in the style guide), while leaving the complex cases for the programmer to optimize as and when needed. Many people who scoff at opinionated style guides forget that in a big company you don't want 1000 engineers to spend time independently figuring out whether X needs to be optimized [1]. If someone can do the profiling, figure out what should be done in 99% of cases, make a style rule out of it explaining the rationale, it saves everyone the hassle. I believe Google does exactly this. [1] Apart from the waste of time, not every engineer might be capable of actually figuring it out. Many of the intricacies of C++ for example aren't immediately obvious even if you have spent years with the language. It's more optimal to hire a few C++ super-experts, have them do the profiling, and come up with style guidance.
- Ididntdothis 7y agoThat’s what I was thinking. With any piece of software where there are performance concerns, be it memory or speed, the first thing you should be doing is to fire up a profiler and get some hard data. I usually profile almost all my stuff just to see if anything unusual or unexpected shows up. This is often a good learning experience.
- hinkley 7y agoThere's an old joke about mathematicians being bad at 'math' (adding, substracting, multiplying). I think we've missed a similar lampoon of software developers about measuring things with computers. Lack of basic analysis skills has been a running lament for about as long as I can remember. Some of it may be learned. We seem to fare better when we have accidents that require the additional expenditure of resources than when we point them out a priori. Letting things be surprises is a bad habit but one that often enough pays dividends.
- mannykannot 7y ago> I had to spend time justifying the presence of that ‘0’ many times over the years as other developers questioned its purpose. One of the better cases for using appropriate comments...
- 1-more 7y agoSlightly zestier idea: make a helper function `makeMostlyEmptyHashSet` and a linter rule that warns on `new HashSet`. You can turn off the linter warning with a comment in every linter I've used, and that comment could include a justification for the default sized `HashSet`.
- egdod 7y agoHaving to justify using a default is a little too zesty for my taste.
- deleted 7y ago[deleted]
- hinkley 7y agoAuthor discusses a Java 1.3 app where he saved tons of memory by reducing leaf collection sizes. I had sort of the opposite with a Java 1.2 code base. Some clever person has noticed that a bunch of vectors often had only 4-7 entries so they overrode the default (10) to 7. This is unfortunately greater than n/2 which will become important momentarily. In Java collections tend to double in size on space exhaustion. Using a multiple instead of addition amortizes the insertion time to O(1). So if you start at 7, you go to 14 next. As I’m sure you can guess, the number of entries crept up to 8 over time. Switching back to the defaults reduced the wasted space from 6 to 2 slots, and dropped our memory usage by more than 10%.
- ping_pong 7y agoTerrible article. But even worse, the "fix" was what I would consider a spaghetti hack. They create a HashSet that is mostly never used? The real fix is to create the HashSet when you know you need it and don't create it if you don't need it. It sounds like the underlying code has expectations that the HashSet exists and is valid, which in and of itself is bad code. Check to see if it's null, and if it is null and you need it to not be null, then allocate it then, with size 0 or default capacity, whichever makes more sense.
- hk__2 7y ago> It sounds like the underlying code has expectations that the HashSet exists and is valid, which in and of itself is bad code. How is that bad code? No NPE and other null-related issues, cleaner API, easier to test.
- remote_phone 7y agoThe fact that it cost $500,000 in real money because of this code is more than enough proof that it’s bad code. Plus it’s a small enough change that if it gets modified again, it will cause further money loss.
- hk__2 7y ago> The fact that it cost $500,000 in real money because of this code is more than enough proof that it’s bad code. If this is a proof of anything, it’s that it can be optimized; not that it’s bad code.
- bcrosby95 7y agoBack in the 00's our servers were suffering under heavy load. Normally they just bought more servers, but I asked if anyone ever profiled our app and no one could remember doing so. I did so and our home grown ORM did a couple really dumb things. Including access the disk each time it allocated a new object that represented a row (wtf). After spending a couple days fixing those things we needed 1/4 of the servers we did beforehand.
- ChrisMarshallNY 7y agoIt seems to mostly be about optimization. There are some good points, but, like so many of these topics, it enters the world of "orthodoxy," where we all have to do things the same way, everywhere, and in all places. Optimization (effective optimization, anyway) is a fairly intense process. For one thing, common sense doesn't really apply. What seems to be an optimization can sometimes do exactly the opposite, like breaking cache, or introducing thread contention. It can also introduce really weird bugs, that are hard to track down, and strange code structures that relatively inexperienced maintenance programmers may have difficulty grokking. It's all about the metrics. Profilers are your friend. Also, in many cases, optimization isn't necessary at all. If the code controls response to a tab selection, then the code that redraws the tab is likely to be executed in a separate thread, anyway, and done at the pleasure of the OS. Why bother optimizing the lookup index to save a few microseconds? Another matter, entirely, when we are iterating an Array that is many thousands of elements long. In some cases, using HOF can actually decrease the performance (but YMMV). I have sometimes had to replace a map() with a for. We measure and find hot spots, and then concentrate on them. If we are working with mobile or embedded, then we also need to worry about power consumption. That's a fairly fraught area, right there, and optimization can actually cause power drain.
- gav 7y ago> It's all about the metrics. Profilers are your friend. In my experience, profilers are not used nearly enough. They also tend to be used when systems are at or near crisis already. It's way more valuable to have profilers run at on every build (or at least every deployment) to see if there's regressions. Having performance metrics in production to continually track key metrics is critical. All software is built with assumptions that impact performance and all of a sudden those can be invalidated. There's a lot of O(N^2) code that's just fine right up until it explodes. Some examples I've run across: - A query API that could contain metadata saying "throw away this result and re-query with these values instead" that worked great for years because it only impacted a single-digit percentage of queries. One day the users decided to really start using this feature, and found that the impact of executing a query that took 100ms and running another 100ms query was way worse than doubling the average. - A system where you could configure plugins to make "pretty" URLs by running a list of regular expressions. Again, this worked great for years until both the number of URLs and the number of plugins increased and the 25% of the page generation time was spent running regular expressions.
- kazinator 7y ago> As I found out while writing this article, modern JDKs do not allocate any space in a HashSet until something is added. Ah, but the object still exists. If large space of objects is needed that is sparsely used, the thing to do is to make that space itself lazy, not the individual objects.
- dekhn 7y agoOne of the more interesting optimizations I've seen is the C++ string class. A class instance takes up 24 bytes, many of which weren't used, so for short strings the data was stored inline rather than having a pointer to external memory. This is big if you have an app that has millions of tiny strings.
- chrchang523 7y agoNote that std::string data representation, and the associated set of small-string optimizations, are highly implementation-dependent; it even differs significantly between clang and gcc's standard libraries. For gcc, std::string size has been 32 bytes on 64-bit targets for the last several years.
- dekhn 7y agoYep. You can see some more context here: http://scottmeyers.blogspot.com/2012/04/stdstring-sso-and-move-semantics.html http://scottmeyers.blogspot.com/2012/04/stdstring-sso-and-mo... I don't know if there are any canonical writeups on the various string implementations and their performance implications wrt modern hardware.
- stygiansonic 7y agoAlthough the article is a bit long-winded as others have pointed out, it did mention the importance of profiling your application before making any changes. In particular, you should always aim to profile your app when running under a production load so that you do not have to make assumptions about its behaviour. Something like async-profiler[0] is good, since it avoids the safepoint bias issue and can also track heap allocations. 0. https://github.com/jvm-profiling-tools/async-profiler https://github.com/jvm-profiling-tools/async-profiler
- enitihas 7y agoIf your java application needs a lot of small collections, it may make sense to use guava immutable collections. They have specialised overloads for lots of sizes, and small collections will not take any more memory than a hardcoded minimal collection.
- mirekrusin 7y agoThe takeaway should be - run profiler at least once before hiring extra architects and spending half a million on hardware/licenses.
- acvny 7y agoClickbait article name.
- unilynx 7y agoThe Windows /3GB bootflag might have also given the needed 0.5GB of extra heap space, but apparently the JVMs couldn't really make use of it according to https://stackoverflow.com/questions/21631167/java-heap-space-and-the-ram https://stackoverflow.com/questions/21631167/java-heap-space...
- redis_mlc 7y agoRandal Schwartz saved more than that by enabling caching in a template for one of Yahoo's properties near LA. So it really is possible. As a DBA, I routinely save that (and more) by doing query performance tuning. I aim for 500x-1,000x faster on physical hardware, and 10,000x in the cloud, thanks to EBS/pSSD latency. And not using Hadoop.
- jandrese 7y agoI have to wonder about an architecture where he's creating thousands of these hash objects and then rarely using them. Initializing them to size 0 was a workaround, but it makes me wonder if maybe he should consider lazy evaluation where the objects are only created if they're actually used? That's still a bunch of CPU cycles burned churning through the setup/teardown of empty hash tables.
- draw_down 7y agoIronically, if this had been raised at the time I bet it would have been shot down as premature optimization!
- ambulancechaser 7y agoThe article mentions that they rarely had elements, not that they were rarely used. They might have been tested for membership in lots of hot paths. There's no way to know. And I bet a lazily-instantiated set might take up even more space than a regular set anyways. And if they are often used, even if not added to, it would have made the problem even worse.
- gwbas1c 7y agoI really want to know more about "why." Specifically, more about why the design of the system leads to creating tons and tons of empty hashsets. Leads me to believe that there's lots more meaningful optimizations remaining that someone who better understands the system could implement.
- paulddraper 7y agoI had a somewhat similar discovery with V8 (JS VM): V8 allocates a whopping 64 bytes per empty object. {} But if you initialize it with a single property, the size shrinks to 40 bytes. [1] {v8Hack: null} I too was creating lots of object that would stay empty, but V8 (incorrectly) assumed I was going to change them later so it reserved some extra space. [2] Unfortunately, there is no size hint like for Java's HashMap; the closest hint you can do is create your objects with dummy properties. [1] https://stackoverflow.com/questions/59044239/why-do-empty-objects-take-more-memory-than-non-empty-ones https://stackoverflow.com/questions/59044239/why-do-empty-ob... [2] https://www.mattzeunert.com/2017/03/29/v8-object-size.html https://www.mattzeunert.com/2017/03/29/v8-object-size.html
- exabrial 7y agoThe form: Boolean b = Boolean.valueOf(true); Is slightly more efficient than the subjectively uglier: Boolean b = new Boolean(true); So couple of notes on that. The "new" operator is cheap, but still involves an allocation. Oddly enough, one of the strangest bugs I found back in the day was: Boolean a = new Boolean(true); Boolean b = new Boolean(true); System.out.println(a == b); false But with objects and wrapper types, the .equals() method is the correct way to compare two objects.
- stygiansonic 7y agoIn my opinion this behaviour shouldn’t be considered strange - “new” always means instantiation of a new object and the “==“ operator for objects always checks for object identity. Since both operands are objects, the above would apply. This is another reason to avoid boxed primitive whenever possible.
- exabrial 7y agoAgree. The "new" operator in my case was hidden deep inside a corporate framework where the guy that invented it had left a long time ago. Quite an adventure down the rabbit hole to figure that out! Boxed primitives are useful when working with systems and languages where "null" is a concept. Take for example, interacting with a database where you have a boolean column that has true/false/null. That translates well to boxed primitive types.
- tudelo 7y agoMost of my Java optimization attempts have ended in 'oh, library x has deadlocks' where library x was necessary to use without massive time investments (big libraries like hibernate, oracle db drivers)...
- phs 7y agoShucks! I came in here guessing it would be adding the trailing period on a domain name, to disable a long list of resolv.conf entries.
- pdfernhout 7y agoBeen trying to get WordPress to optimize memory usage for four years on this ticket: https://core.trac.wordpress.org/ticket/34560 https://core.trac.wordpress.org/ticket/34560 "Right now, you will unhappily see "500 server error" and "white screen of death" results eventually if you, say, edit a 400K page for hundreds of revisions, even with a 256MB server memory limit. ... There are apparently three requests for all posts versions being made by the (Page) editor." That might be some record both for inefficient coding and then for not caring much about inefficient coding and all the extra expense millions of WordPress users face to host such inefficient code? Either that or maybe few WordPress users make many edits to large pages?